Compare commits

...

20 Commits

Author SHA1 Message Date
cwilvx
cf2d9537ff fix: double click on tracks is now working
+ fix see all link not being shown in favorites and artist page
2025-08-14 21:14:18 +03:00
cwilvx
6f4a59f971 Add collections display to backup UI and refactor url config 2025-06-17 23:06:45 +03:00
cwilvx
7b21853f97 conditionally upgrade requests on https 2025-05-13 21:17:59 +03:00
cwilvx
663dbd2a7c upgrade insecure requests 2025-05-13 00:15:16 +03:00
cwilvx
c7a0b5ab7e remove console.log 2025-05-10 18:27:24 +03:00
cwilvx
ad8eeb7a2a try: dynamic host resolving 2025-05-10 17:49:07 +03:00
cwilvx
e799c96872 add settings item to toggle playlists in folder view 2025-04-21 21:41:33 +03:00
cwilvx
234aed54d7 fix: favorites card zindex issue 2025-04-03 14:21:27 +03:00
cwilvx
574d7fd5e7 fix artist page track limit 2025-03-15 22:52:40 +03:00
cwilvx
4a1106d784 fix ending silence null error 2025-03-13 09:49:54 +03:00
cwilvx
d9f7e5fb14 fix favorites card 2025-03-10 12:23:38 +03:00
cwilvx
571c4a5264 disable: checking if lyrics exist 2025-03-03 11:35:33 +03:00
cwilvx
e71bc7164c cleanup page -> collectoin 2025-03-01 16:55:23 +03:00
cwilvx
77f18ac640 move to #000 2025-02-28 20:34:52 +03:00
cwilvx
78d57a64b9 fix repeat mode 2025-02-27 23:59:20 +03:00
cwilvx
ff502521e8 fix: remove traces of "page" 2025-02-27 13:31:41 +03:00
cwilvx
7caa70b9d6 remove console logs 2025-02-26 14:39:28 +03:00
cwilvx
cc3b372090 update inline fav icon defaults 2025-02-26 00:10:06 +03:00
cwilvx
c297f75132 improve: inline heart icon
+ rename pages to collections
2025-02-25 23:29:05 +03:00
cwilvx
7c954ef805 fix: artists not showing on search artist tab 2025-02-25 21:22:59 +03:00
44 changed files with 875 additions and 712 deletions

View File

@@ -4,7 +4,6 @@
- Check out the mobile sidebar and navbar
- Remove old settings page files
- Fix: track loading indicator in bottom bar
- Unfuck javascript controlled responsiveness
- Redesign the album page header for mobile
@@ -14,7 +13,6 @@
- Add trailing slash to folder url accessed from the breadcrumb
- Clip the browseable items on the homepage
- Fix: The responsiveness glitch between 900px - 964px 😅
- Fix: Queue repeat
- Make All Albums/Artists view sort banner sticky
# DONE ✅

View File

@@ -57,6 +57,7 @@ $g-border: solid 1px $gray5;
.b-bar {
grid-area: bottombar;
border-top: $g-border;
// background-color: $bars;
}
.content-page {
@@ -127,7 +128,8 @@ $g-border: solid 1px $gray5;
}
.topnav {
background-color: $gray;
// background-color: $bars;
border-bottom: $g-border;
}
.vue-recycle-scroller,

View File

@@ -21,13 +21,13 @@ $content-padding-bottom: 2rem;
$black: #181a1c;
$white: #ffffffde;
$gray: #1c1c1e;
$gray: #1a1919;
$gray1: #8e8e93;
$gray2: #636366;
$gray3: #48484a;
$gray4: #3a3a3c;
$gray5: #2c2c2e;
$body: #111111;
$body: #000;
$red: #f7635c;
$blue: #0a84ff;
@@ -41,6 +41,7 @@ $brown: #ac8e68;
$indigo: #5e5ce6;
$teal: rgb(64, 200, 224);
$lightbrown: #ebca89;
$bars: #111111;
$primary: $gray4;
$accent: $gray1;

View File

@@ -63,7 +63,6 @@ function handleFav() {
<style lang="scss">
.b-bar {
background-color: rgb(22, 22, 22);
display: grid;
grid-template-columns: 1fr max-content 1fr;
align-items: center;

View File

@@ -4,11 +4,11 @@
<Volume />
<button
class="repeat"
:class="{ 'repeat-disabled': settings.no_repeat }"
:title="settings.repeat_all ? 'Repeat all' : settings.no_repeat ? 'No repeat' : 'Repeat one'"
:class="{ 'repeat-disabled': settings.repeat == 'none' }"
:title="settings.repeat == 'all' ? 'Repeat all' : settings.repeat == 'one' ? 'Repeat one' : 'No repeat'"
@click="settings.toggleRepeatMode"
>
<RepeatOneSvg v-if="settings.repeat_one" />
<RepeatOneSvg v-if="settings.repeat == 'one'" />
<RepeatAllSvg v-else />
</button>
<button title="Shuffle" @click="queue.shuffleQueue">

View File

@@ -94,7 +94,7 @@ const res_type = computed(() => {
type It = Album & Artist & Track
const item = computed(() => {
return top_results.value.top_result.item as It
return top_results.value.top_result as It
})
const context_menu_showing = ref(false)
@@ -106,7 +106,7 @@ function showMenu(e: MouseEvent) {
<style lang="scss">
.top-result-item {
background-color: $gray5;
background-color: $gray;
padding: 1rem;
display: grid;
gap: 1rem;

View File

@@ -30,6 +30,10 @@
<div class="item__favorites">
{{ backup.favorites }} favorite{{ backup.favorites !== 1 ? 's' : '' }}
</div>
<div class="item__collections">
{{ backup.collections }} collection{{ backup.collections !== 1 ? 's' : '' }}
</div>
</div>
</div>
<div class="buttons">
@@ -55,6 +59,7 @@ interface Backup {
playlists: number
scrobbles: number
favorites: number
collections: number
date: string
}
const backups = ref<Backup[]>([])

View File

@@ -1,5 +1,5 @@
<template>
<div class="statitem" :class="props.icon">
<div class="statitem" :class="props.icon" :style="dynamicBackgroundStyle">
<svg
class="noise"
xmlns="http://www.w3.org/2000/svg"
@@ -35,7 +35,7 @@
surfaceScale="21"
specularConstant="1.7"
specularExponent="20"
lighting-color="#7957A8"
lighting-color="transparent"
x="0%"
y="0%"
width="100%"
@@ -50,20 +50,20 @@
<rect width="700" height="700" fill="transparent"></rect>
<rect width="700" height="700" fill="#7957a8" filter="url(#nnnoise-filter)"></rect>
</svg>
<div class="itemcontent">
<div class="itemcontent" :style="{ color: textColor }">
<div class="count ellip2" :title="formattedValue">{{ formattedValue }}</div>
<div class="title">{{ text }}</div>
</div>
<component :is="icon" class="staticon" v-if="!props.icon.startsWith('top')" />
<component :is="icon" v-if="!props.icon.startsWith('top')" class="staticon" :style="{ color: textColor }" />
<router-link
v-if="props.icon.startsWith('top') && props.image"
:to="{
name: Routes.album,
params: {
albumhash: props.image?.replace('.webp', ''),
},
}"
v-if="props.icon.startsWith('top') && props.image"
>
<img class="staticon statimage shadow-sm" :src="paths.images.thumb.small + props.image" alt="" />
</router-link>
@@ -81,6 +81,11 @@ import SparklesSvg from '@/assets/icons/sparkles.svg'
import { paths } from '@/config'
import { Routes } from '@/router'
import useArtistStore from '@/stores/pages/artist'
import useAlbumStore from '@/stores/pages/album'
import { storeToRefs } from 'pinia'
import { useRoute } from 'vue-router'
import { getTextColor } from '@/utils/colortools/shift'
const props = defineProps<{
value: string
@@ -89,6 +94,13 @@ const props = defineProps<{
image?: string
}>()
// Get current route and colors from stores
const route = useRoute()
const artistStore = useArtistStore()
const albumStore = useAlbumStore()
const { colors: artistColors } = storeToRefs(artistStore)
const { colors: albumColors } = storeToRefs(albumStore)
const icon = computed(() => {
switch (props.icon) {
case 'streams':
@@ -110,6 +122,61 @@ const icon = computed(() => {
const formattedValue = computed(() => {
return props.value.toLocaleString()
})
// Determine which dynamic color to use based on current route
const dynamicColor = computed(() => {
switch (route.name) {
// Album-related pages should use album colors
case Routes.album:
return albumColors.value?.bg || null
// Artist-related pages should use artist colors
case Routes.artist:
return artistColors.value?.bg || null
// All other pages should use default colors
default:
return null
}
})
// Default hardcoded background styles
const defaultBackgroundStyles = computed(() => {
switch (props.icon) {
case 'streams':
return 'linear-gradient(to top, #c79081 0%, #dfa579 100%)'
case 'playtime':
return 'linear-gradient(-225deg, #3d4e81 0%, #5753c9 48%, #6e7ff3 100%)'
case 'trackcount':
return 'linear-gradient(to top, #6a66b9 0%, #7777db 52%, #7b7bd4 100%)'
case 'toptrack':
return 'linear-gradient(-225deg, #65379b 0%, #6750b3 53%, #6457c6 100%)'
default:
return 'linear-gradient(to top right, rgb(120, 76, 129), #9643da91, rgb(132, 80, 228))'
}
})
// Computed style that uses dynamic color or falls back to hardcoded
const dynamicBackgroundStyle = computed(() => {
if (dynamicColor.value) {
return {
backgroundColor: dynamicColor.value,
backgroundImage: 'none',
}
}
return {
backgroundImage: defaultBackgroundStyles.value,
}
})
// Computed text color based on background using the same logic as headers
const textColor = computed(() => {
if (dynamicColor.value) {
return getTextColor(dynamicColor.value)
}
// Return default white color when using gradients
return '#ffffff'
})
</script>
<style lang="scss">
@@ -121,25 +188,10 @@ const formattedValue = computed(() => {
aspect-ratio: 1;
overflow: hidden;
// Default background - will be overridden by dynamic styles
background-image: linear-gradient(to top right, rgb(120, 76, 129), #9643da91, rgb(132, 80, 228));
position: relative;
&.streams {
background-image: linear-gradient(to top, #c79081 0%, #dfa579 100%);
}
&.playtime {
background-image: linear-gradient(-225deg, #3d4e81 0%, #5753c9 48%, #6e7ff3 100%);
}
&.trackcount {
background-image: linear-gradient(to top, #6a66b9 0%, #7777db 52%, #7b7bd4 100%);
}
&.toptrack {
background-image: linear-gradient(-225deg, #65379b 0%, #6750b3 53%, #6457c6 100%);
}
.itemcontent {
position: relative;
z-index: 1;

View File

@@ -1,30 +1,31 @@
<template>
<form action="" v-if="delete">
<div>Are you sure you want to delete this page?</div>
<br>
<div>Are you sure you want to delete this collection?</div>
<br />
<button @click.prevent="submit" class="critical">Yes, Delete</button>
</form>
<form class="playlist-modal" @submit.prevent="submit" v-else>
<label for="name">Page name</label>
<label for="name">Collection name</label>
<br />
<input type="search" class="rounded-sm" id="name" :value="page?.name" />
<input type="search" class="rounded-sm" id="name" :value="collection?.name" />
<br />
<label for="description">Description</label>
<br />
<input type="search" class="rounded-sm" id="description" :value="page?.extra.description" />
<input type="search" class="rounded-sm" id="description" :value="collection?.extra.description" />
<br /><br />
<button type="submit">{{ page ? 'Update' : 'Create' }}</button>
<button type="submit">{{ collection ? 'Update' : 'Create' }}</button>
</form>
</template>
<script setup lang="ts">
import { Page } from '@/interfaces'
import { createNewPage, deletePage, updatePage } from '@/requests/pages'
import { router } from '@/router';
import { Collection } from '@/interfaces'
import { createNewCollection, deleteCollection, updateCollection } from '@/requests/collections'
import { router } from '@/router'
import { NotifType, Notification } from '@/stores/notification'
const props = defineProps<{
page?: Page
collection?: Collection
hash?: string
type?: string
extra?: any
@@ -36,13 +37,13 @@ const emit = defineEmits<{
(e: 'setTitle', title: string): void
}>()
emit('setTitle', props.page ? (props.delete ? 'Delete Page' : 'Update Page') : 'New Page')
emit('setTitle', (props.collection ? (props.delete ? 'Delete' : 'Update') : 'New') + ' Collection')
async function submit(e: Event) {
if (props.delete && props.page) {
const deleted = await deletePage(props.page.id)
if (props.delete && props.collection) {
const deleted = await deleteCollection(props.collection.id)
if (deleted) {
new Notification('Page deleted', NotifType.Success)
new Notification('Collection deleted', NotifType.Success)
emit('hideModal')
router.push('/')
}
@@ -54,8 +55,8 @@ async function submit(e: Event) {
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, [
if (props.collection == null) {
const created = await createNewCollection(name, description, [
{
hash: props.hash as string,
type: props.type as string,
@@ -64,16 +65,16 @@ async function submit(e: Event) {
])
if (created) {
new Notification('New page created', NotifType.Success)
new Notification('New collection created', NotifType.Success)
emit('hideModal')
}
} else {
const updatedPage = await updatePage(props.page, name, description)
const updatedPage = await updateCollection(props.collection, name, description)
if (updatedPage) {
props.page.name = updatedPage.name
props.page.extra.description = updatedPage.extra.description
new Notification('Page updated', NotifType.Success)
props.collection.name = updatedPage.name
props.collection.extra.description = updatedPage.extra.description
new Notification('Collection updated', NotifType.Success)
emit('hideModal')
}
}

View File

@@ -7,12 +7,19 @@
{{ title }}
</RouterLink>
</b>
<SeeAll v-if="route && itemlist.length >= maxAbumCards" :route="route" :text="seeAllText" />
<!-- INFO: This SEE ALL is shown when there's no description. Eg. in favorites page -->
<SeeAll
v-if="!description && route && itemlist.length >= maxAbumCards"
:route="route"
:text="seeAllText"
/>
</div>
<div v-if="description" class="rdesc">
<RouterLink :to="route || ''">
{{ description }}
</RouterLink>
<!-- INFO: This SEE ALL is shown when there's a description. Eg. in the home page -->
<SeeAll v-if="route && itemlist.length >= maxAbumCards" :route="route" :text="seeAllText" />
</div>
</div>
<div class="recentitems">
@@ -90,7 +97,7 @@ function getComponent(type: string) {
return FolderCard
case 'playlist':
return PlaylistCard
case 'favorite_tracks':
case 'favorite':
return FavoritesCard
case 'mix':
return MixCard
@@ -127,7 +134,7 @@ function getProps(item: { type: string; item?: any; with_helptext?: boolean }) {
return {
playlist: item.item,
}
case 'favorite_tracks':
case 'favorite':
return {
item: item.item,
}
@@ -171,6 +178,9 @@ function getProps(item: { type: string; item?: any; with_helptext?: boolean }) {
.rdesc {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.747);
display: flex;
align-items: baseline;
justify-content: space-between;
}
}

View File

@@ -1,80 +1,126 @@
<template>
<RouterLink :to="{ name: Routes.favoriteTracks }" class="favoritescard rounded">
<div class="img">
<svg width="100" height="100" viewBox="0 0 28 28" fill="#ff453a" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9912 22.1445C14.2197 22.1445 14.5449 21.9775 14.8086 21.8105C19.7217 18.6465 22.8682 14.9375 22.8682 11.1758C22.8682 7.9502 20.6445 5.7002 17.8408 5.7002C16.0918 5.7002 14.7822 6.66699 13.9912 8.11719C13.2178 6.67578 11.8994 5.7002 10.1504 5.7002C7.34668 5.7002 5.11426 7.9502 5.11426 11.1758C5.11426 14.9375 8.26074 18.6465 13.1738 21.8105C13.4463 21.9775 13.7715 22.1445 13.9912 22.1445Z"
/>
</svg>
<PlayBtn :source="playSources.favorite" />
</div>
<div class="info">
<div class="rhelp playlist">
<span class="help">PLAYLIST</span>
<span class="time">{{ item.time }}</span>
</div>
<div class="title">Favorite Tracks</div>
<div class="fcount">
<b>{{ item.count + ` Track${item.count == 1 ? "" : "s"}` }}</b>
</div>
</div>
</RouterLink>
<RouterLink :to="{ name: Routes.favoriteTracks }" class="favoritescard rounded">
<div class="img">
<div class="blur" :style="{ backgroundImage: `url(${paths.images.thumb.small + item.image})` }"></div>
</div>
<div class="overlay">
<PlayBtn :source="playSources.favorite" />
<svg
class="heart"
width="100"
height="100"
viewBox="0 0 28 28"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
:style="{ color: color }"
<path
d="M13.9912 22.1445C14.2197 22.1445 14.5449 21.9775 14.8086 21.8105C19.7217 18.6465 22.8682 14.9375 22.8682 11.1758C22.8682 7.9502 20.6445 5.7002 17.8408 5.7002C16.0918 5.7002 14.7822 6.66699 13.9912 8.11719C13.2178 6.67578 11.8994 5.7002 10.1504 5.7002C7.34668 5.7002 5.11426 7.9502 5.11426 11.1758C5.11426 14.9375 8.26074 18.6465 13.1738 21.8105C13.4463 21.9775 13.7715 22.1445 13.9912 22.1445Z"
/>
</svg>
</div>
<div class="info">
<div class="rhelp playlist">
<span class="help">PLAYLIST</span>
<span class="time">{{ item.time }}</span>
</div>
<div class="title">Favorite Tracks</div>
<div class="fcount">
<b>{{ item.count + ` Track${item.count == 1 ? '' : 's'}` }}</b>
</div>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { playSources } from "@/enums";
import { Routes } from "@/router";
import PlayBtn from "../shared/PlayBtn.vue";
import { paths } from '@/config'
import { Routes } from '@/router'
import { playSources } from '@/enums'
import PlayBtn from '../shared/PlayBtn.vue'
defineProps<{
item: any;
}>();
item: {
time: string
count: number
image: string
}
}>()
</script>
<style lang="scss">
.favoritescard {
padding: $medium;
.img {
width: 100%;
aspect-ratio: 1/1;
background-color: $gray5;
border-radius: $small;
margin-bottom: $medium;
display: flex;
align-items: center;
background-image: linear-gradient(37deg, $gray5, $gray, $gray);
justify-content: center;
padding: $medium;
position: relative;
}
.play-btn {
position: absolute;
width: 4rem;
bottom: 0;
opacity: 0;
transition: all 0.25s;
}
.img,
.overlay {
width: 100%;
aspect-ratio: 1/1;
border-radius: $small;
margin-bottom: $medium;
}
.fcount {
font-size: 0.8rem;
opacity: 0.75;
padding-top: 2px;
}
.img {
overflow: hidden;
&:hover {
background-color: $gray4;
.blur {
height: 100%;
width: 100%;
background-image: linear-gradient(37deg, $gray5, $gray, $gray);
// background-image: url('http://localhost:1980/img/thumbnail/xsmall/e74d8c49e8d6340f.webp?pathhash=24bf8142d7150965');
background-size: cover;
background-position: center;
filter: brightness(0.5) blur(15px);
background-color: rgba(0, 0, 0, 0.5);
overflow: hidden;
opacity: 0.5;
}
}
.overlay {
display: flex;
align-items: center;
justify-content: center;
$size: calc(100% - $medium * 2);
position: absolute;
top: $medium;
left: $medium;
width: $size;
z-index: 1;
}
.heart {
color: $pink;
}
.play-btn {
opacity: 1;
transform: translateY(-1rem);
position: absolute;
width: 4rem;
bottom: 0;
opacity: 0;
transition: all 0.25s;
}
}
.info {
.title {
font-weight: 600;
font-size: 0.95rem;
.fcount {
font-size: 0.8rem;
opacity: 0.75;
padding-top: 2px;
}
&:hover {
background-color: $gray4;
.play-btn {
opacity: 1;
transform: translateY(-1rem);
}
}
.info {
.title {
font-weight: 600;
font-size: 0.95rem;
}
}
}
}
</style>

View File

@@ -2,10 +2,17 @@
<div
class="songlist-item rounded-sm"
:class="[{ current: isCurrent() }, { contexton: context_menu_showing }]"
@dblclick.prevent="emitUpdate"
@dblclick="emitUpdate"
@contextmenu.prevent="showMenu"
>
<TrackIndex v-if="!isSmall" :index="index" :is_fav="is_fav" @add-to-fav="addToFav(track.trackhash)" />
<TrackIndex
v-if="!isSmall"
:index="index"
:is_fav="is_fav"
:show-inline-fav-icon="settings.showInlineFavIcon"
@add-to-fav="addToFav(track.trackhash)"
/>
<TrackTitle
:track="track"
:is_current="isCurrent()"
@@ -23,10 +30,13 @@
/>
<TrackDuration
:duration="track.duration || 0"
@showMenu="showMenu"
:help_text="track.help_text"
:is_fav="is_fav"
:showFavIcon="!isFavoritesPage"
:showInlineFavIcon="settings.showInlineFavIcon"
:highlightFavoriteTracks="settings.highlightFavoriteTracks"
@showMenu="showMenu"
@toggleFav="addToFav(track.trackhash)"
/>
</div>
</template>
@@ -47,7 +57,9 @@ import TrackAlbum from './SongItem/TrackAlbum.vue'
import TrackDuration from './SongItem/TrackDuration.vue'
import TrackIndex from './SongItem/TrackIndex.vue'
import TrackTitle from './SongItem/TrackTitle.vue'
import useSettings from '@/stores/settings'
const settings = useSettings()
const context_menu_showing = ref(false)
const queue = useQueueStore()
@@ -131,9 +143,9 @@ const isFavoritesPage = route.path.startsWith('/favorites')
transition: background-color 0.2s ease-out;
&:hover {
background-color: $gray5;
background-color: $gray;
.index {
.index.ready {
.text {
transition-delay: 400ms;
@@ -157,6 +169,10 @@ const isFavoritesPage = route.path.startsWith('/favorites')
.song-duration.help-text {
opacity: 1;
}
.options-and-duration .heart-icon.showInlineFavIcon {
display: block;
}
}
.index {

View File

@@ -1,6 +1,11 @@
<template>
<div class="options-and-duration">
<div v-if="is_fav && showFavIcon !== false" class="heart-icon is-favorited">
<div
v-if="showInlineFavIcon"
class="heart-icon"
:class="{ showInlineFavIcon, 'is_fav': is_fav && highlightFavoriteTracks }"
@click.stop="$emit('toggleFav')"
>
<HeartSvg :state="is_fav" :no_emit="true" />
</div>
<div class="song-duration" :class="{ has_help_text: help_text }">{{ formatSeconds(duration) }}</div>
@@ -21,12 +26,15 @@ import HeartSvg from '../HeartSvg.vue'
defineProps<{
duration: number
is_fav: boolean
showInlineFavIcon: boolean
highlightFavoriteTracks: boolean
showFavIcon?: boolean
help_text?: string
}>()
defineEmits<{
(e: 'showMenu', event: MouseEvent): void
(e: 'toggleFav'): void
}>()
</script>
@@ -39,23 +47,24 @@ defineEmits<{
margin-right: $small;
position: relative;
@include allPhones {
gap: $small;
}
@include mediumPhones {
> .heart-icon.is-favorited {
display: none;
}
}
> .heart-icon.is-favorited {
display: block;
.heart-icon {
display: none;
width: 28px;
height: 28px;
user-select: none;
pointer-events: none;
transition: opacity 0.2s ease-out;
transform: scale(0.8);
margin-right: $small;
svg {
color: $red;
}
@include mediumPhones {
display: none;
@@ -66,6 +75,10 @@ defineEmits<{
}
}
.heart-icon.is_fav {
display: block;
}
.song-duration {
font-size: small;
font-variant-numeric: tabular-nums;

View File

@@ -3,11 +3,12 @@
class="index t-center ellip"
@click.prevent="$emit('addToFav')"
@dblclick.prevent.stop="() => {}"
:class="{ 'ready': !showInlineFavIcon }"
>
<div class="text">
{{ index }}
</div>
<div class="heart-icon">
<div class="heart-icon" v-if="!showInlineFavIcon">
<HeartSvg :state="is_fav" :no_emit="true" />
</div>
</div>
@@ -19,6 +20,7 @@ import HeartSvg from "../HeartSvg.vue";
defineProps<{
index: number | string;
is_fav: boolean | undefined;
showInlineFavIcon: boolean;
}>();
defineEmits<{
@@ -53,7 +55,6 @@ defineEmits<{
transition: all 0.2s;
transform: translateX(-1.5rem);
button {
border: none;
width: 2rem;

View File

@@ -1,17 +1,20 @@
import axios from 'axios'
const development = import.meta.env.DEV
export function getBaseUrl() {
const base_url = window.location.origin
if (!development) {
return base_url
return ''
}
const base_url = window.location.origin
const splits = base_url.split(':')
return base_url.replace(splits[splits.length - 1], '1980')
}
const base_url = getBaseUrl()
axios.defaults.baseURL = base_url
const baseImgUrl = base_url + '/img'
const imageRoutes = {
@@ -31,7 +34,7 @@ const imageRoutes = {
export const paths = {
api: {
favorites: base_url + '/favorites',
favorites: '/favorites',
get favAlbums() {
return this.favorites + '/albums'
},
@@ -50,15 +53,15 @@ export const paths = {
get removeFavorite() {
return this.favorites + '/remove'
},
artist: base_url + '/artist',
lyrics: base_url + '/lyrics',
plugins: base_url + '/plugins',
artist: '/artist',
lyrics: '/lyrics',
plugins: '/plugins',
get mixes() {
return this.plugins + '/mixes'
},
// Single album
album: base_url + '/album',
album: '/album',
get albumartists() {
return this.album + '/artists'
},
@@ -72,12 +75,12 @@ export const paths = {
return this.album + '/other-versions'
},
folder: {
base: base_url + '/folder',
showInFiles: base_url + '/folder/show-in-files',
base: '/folder',
showInFiles: '/folder/show-in-files',
},
dir_browser: base_url + '/folder/dir-browser',
dir_browser: '/folder/dir-browser',
playlist: {
base: base_url + '/playlists',
base: '/playlists',
get new() {
return this.base + '/new'
},
@@ -85,11 +88,11 @@ export const paths = {
return this.base + '/artists'
},
},
pages: {
base: base_url + '/pages',
collections: {
base: '/collections',
},
search: {
base: base_url + '/search',
base: '/search',
get top() {
return this.base + '/top?q='
},
@@ -107,13 +110,13 @@ export const paths = {
},
},
logger: {
base: base_url + '/logger',
base: '/logger',
get logTrack() {
return this.base + '/track/log'
},
},
getall: {
base: base_url + '/getall',
base: '/getall',
get albums() {
return this.base + '/albums'
},
@@ -122,7 +125,7 @@ export const paths = {
},
},
colors: {
base: base_url + '/colors',
base: '/colors',
get album() {
return this.base + '/album'
},
@@ -145,9 +148,9 @@ export const paths = {
return this.base + '/update'
},
},
files: base_url + '/file',
files: '/file',
home: {
base: base_url + '/nothome',
base: '/nothome',
get recentlyAdded() {
return this.base + '/recents/added'
},
@@ -186,7 +189,7 @@ export const paths = {
},
},
backups: {
base: base_url + '/backup',
base: '/backup',
get get_backups() {
return this.base + '/list'
},
@@ -201,7 +204,7 @@ export const paths = {
},
},
stats: {
base: base_url + '/logger',
base: '/logger',
get topArtists() {
return this.base + '/top-artists'
},

View File

@@ -1,16 +1,16 @@
import { router, Routes } from '@/router'
import useAlbum from '@/stores/pages/album'
import useCollection from '@/stores/pages/collections'
import useTracklist from '@/stores/queue/tracklist'
import usePage from '@/stores/pages/page'
import { getAlbumTracks } from '@/requests/album'
import { addOrRemoveItemFromCollection } from '@/requests/collections'
import { addAlbumToPlaylist } from '@/requests/playlists'
import { addOrRemoveItemFromPage } from '@/requests/pages'
import { Album, Option, Page, Playlist, Track } from '@/interfaces'
import { AddToQueueIcon, DeleteIcon, PlayNextIcon, PlusIcon } from '@/icons'
import { getAddToPageOptions, getAddToPlaylistOptions, get_find_on_social } from './utils'
import { Album, Collection, Option, Playlist, Track } from '@/interfaces'
import { get_find_on_social, getAddToCollectionOptions, getAddToPlaylistOptions } from './utils'
export default async (album?: Album) => {
const albumStore = useAlbum()
@@ -66,15 +66,15 @@ export default async (album?: Album) => {
icon: PlusIcon,
}
const addToPageAction = (page: Page) => {
addOrRemoveItemFromPage(page.id, album, 'album', 'add')
const addToPageAction = (page: Collection) => {
addOrRemoveItemFromCollection(page.id, album, 'album', 'add')
}
const add_to_page: Option = {
label: 'Add to Page',
label: 'Add to Collection',
children: () =>
getAddToPageOptions(addToPageAction, {
page: null,
getAddToCollectionOptions(addToPageAction, {
collection: null,
hash: album.albumhash,
type: 'album',
extra: {},
@@ -83,17 +83,17 @@ export default async (album?: Album) => {
}
const remove_from_page: Option = {
label: 'Remove from Page',
label: 'Remove item',
action: async () => {
const success = await addOrRemoveItemFromPage(
parseInt(router.currentRoute.value.params.page as string),
const success = await addOrRemoveItemFromCollection(
parseInt(router.currentRoute.value.params.collection as string),
album,
'album',
'remove'
)
if (success) {
usePage().removeLocalItem(album, 'album')
useCollection().removeLocalItem(album, 'album')
}
},
icon: DeleteIcon,

View File

@@ -1,16 +1,15 @@
import { Routes } from '@/router'
import { router } from '@/router'
import { Routes, router } from '@/router'
import usePage from '@/stores/pages/page'
import useCollection from '@/stores/pages/collections'
import useTracklist from '@/stores/queue/tracklist'
import { getArtistTracks } from '@/requests/artists'
import { addOrRemoveItemFromCollection } from '@/requests/collections'
import { addArtistToPlaylist } from '@/requests/playlists'
import { addOrRemoveItemFromPage } from '@/requests/pages'
import { Artist, Option, Page, Playlist } from '@/interfaces'
import { AddToQueueIcon, DeleteIcon, PlayNextIcon, PlusIcon } from '@/icons'
import { getAddToPageOptions, getAddToPlaylistOptions, get_find_on_social } from './utils'
import { Artist, Collection, Option, Playlist } from '@/interfaces'
import { getAddToCollectionOptions, getAddToPlaylistOptions, get_find_on_social } from './utils'
export default async (artisthash: string, artistname: string) => {
const play_next = <Option>{
@@ -50,9 +49,9 @@ export default async (artisthash: string, artistname: string) => {
icon: PlusIcon,
}
const addToPageAction = (page: Page) => {
addOrRemoveItemFromPage(
page.id,
const addToCollectionAction = (collection: Collection) => {
addOrRemoveItemFromCollection(
collection.id,
{
artisthash,
} as Artist,
@@ -62,10 +61,10 @@ export default async (artisthash: string, artistname: string) => {
}
const add_to_page: Option = {
label: 'Add to Page',
label: 'Add to Collection',
children: () =>
getAddToPageOptions(addToPageAction, {
page: null,
getAddToCollectionOptions(addToCollectionAction, {
collection: null,
hash: artisthash,
type: 'artist',
extra: {},
@@ -73,11 +72,11 @@ export default async (artisthash: string, artistname: string) => {
icon: PlusIcon,
}
const remove_from_page: Option = {
label: 'Remove from Page',
const remove_from_collection: Option = {
label: 'Remove item',
action: async () => {
const success = await addOrRemoveItemFromPage(
parseInt(router.currentRoute.value.params.page as string),
const success = await addOrRemoveItemFromCollection(
parseInt(router.currentRoute.value.params.collection as string),
{
artisthash,
} as Artist,
@@ -86,7 +85,7 @@ export default async (artisthash: string, artistname: string) => {
)
if (success) {
usePage().removeLocalItem({ artisthash } as Artist, 'artist')
useCollection().removeLocalItem({ artisthash } as Artist, 'artist')
}
},
icon: DeleteIcon,
@@ -96,7 +95,7 @@ export default async (artisthash: string, artistname: string) => {
play_next,
add_to_queue,
add_to_playlist,
...[router.currentRoute.value.name === Routes.Page ? remove_from_page : add_to_page],
...[router.currentRoute.value.name === Routes.Page ? remove_from_collection : add_to_page],
get_find_on_social('artist'),
]
}

View File

@@ -1,10 +1,9 @@
import modal from '@/stores/modal'
import useAlbum from '@/stores/pages/album'
import useArtist from '@/stores/pages/artist'
import { SearchIcon } from '@/icons'
import { Album, Option, Page, Playlist } from '@/interfaces'
import { getAllPages } from '@/requests/pages'
import { Album, Collection, Option, Playlist } from '@/interfaces'
import { getAllCollections } from '@/requests/collections'
import { getAllPlaylists } from '@/requests/playlists'
export const separator: Option = {
@@ -20,11 +19,11 @@ export function get_new_playlist_option(new_playlist_modal_props: any = {}): Opt
}
}
export function get_new_page_option(new_playlist_modal_props: any = {}): Option {
export function get_new_collection_option(new_collection_modal_props: any = {}): Option {
return {
label: 'New page',
label: 'New Collection',
action: () => {
modal().showPageModal(new_playlist_modal_props)
modal().showCollectionModal(new_collection_modal_props)
},
}
}
@@ -67,28 +66,31 @@ export async function getAddToPlaylistOptions(addToPlaylist: action, new_playlis
* @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()
export async function getAddToCollectionOptions(
addToCollection: (collection: Collection) => void,
new_page_modal_props: any = {}
) {
const new_page = get_new_collection_option(new_page_modal_props)
const data = await getAllCollections()
let items = [new_page]
if (p.length === 0) {
if (data.length === 0) {
return items
}
let pages = <Option[]>[]
let collections = <Option[]>[]
pages = p.map(playlist => {
collections = data.map(collection => {
return <Option>{
label: playlist.name,
label: collection.name,
action: () => {
addToPage(playlist)
addToCollection(collection)
},
}
})
return [...items, separator, ...pages]
return [...items, separator, ...collections]
}
export const get_find_on_social = (page = 'album', query = '', album?: Album) => {

View File

@@ -105,4 +105,5 @@ export interface DBSettings {
lastfmApiKey: string;
lastfmApiSecret: string;
lastfmSessionKey: string;
showPlaylistsInFolderView: boolean;
}

View File

@@ -84,12 +84,10 @@ export async function playFromFolderCard(folderpath: string) {
export async function playFromFavorites(track: Track | undefined) {
const queue = useQueue()
const tracklist = useTracklist()
console.log(track)
// if our tracklist is not from favorites, we need to fetch the favorites
if (tracklist.from.type !== FromOptions.favorite) {
const res = await getFavTracks(0, -1)
console.log(res)
tracklist.setFromFav(res.tracks)
}
@@ -99,7 +97,6 @@ export async function playFromFavorites(track: Track | undefined) {
index = tracklist.tracklist.findIndex(t => t.trackhash === track?.trackhash)
}
console.log(tracklist.tracklist)
queue.play(index)
}

View File

@@ -29,6 +29,7 @@ export interface Track extends AlbumDisc {
filetype: string
is_favorite: boolean
explicit: boolean
type?: string
og_title: string
og_album: string
@@ -133,6 +134,7 @@ export interface Artist {
help_text?: string
time?: string
genres: Genre[]
type?: string
// available in charts
trend?: {
@@ -180,7 +182,7 @@ export interface Playlist {
}[]
}
export interface Page {
export interface Collection {
id: number
name: string
items: (Album | Artist | Mix | Playlist)[]

View File

@@ -3,7 +3,7 @@ import { Album, Artist, Genre, StatItem, Track } from '@/interfaces'
import { NotifType, useToast } from '@/stores/notification'
import useAxios from './useAxios'
export const getArtistData = async (hash: string, limit: number = 5, albumlimit: number = 7) => {
export const getArtistData = async (hash: string, limit: number = 15, albumlimit: number = 7) => {
interface ArtistData {
artist: Artist
tracks: Track[]
@@ -19,7 +19,7 @@ export const getArtistData = async (hash: string, limit: number = 5, albumlimit:
const { data, error, status } = await useAxios({
method: 'GET',
url: paths.api.artist + `/${hash}?limit=${limit}&albumlimit=${albumlimit}`,
url: paths.api.artist + `/${hash}?tracklimit=${limit}&albumlimit=${albumlimit}`,
})
if (status == 404) {

View File

@@ -3,7 +3,7 @@ import useAxios from './useAxios'
import { User, UserSimplified } from '@/interfaces'
export async function getAllUsers<T extends boolean>(simple: T = true as T) {
interface res {
interface Response {
users: T extends true ? UserSimplified[] : User[]
settings: { [key: string]: any }
}
@@ -13,7 +13,7 @@ export async function getAllUsers<T extends boolean>(simple: T = true as T) {
})
if (res.status === 200) {
return res.data as res
return res.data as Response
}
if (res.status === 401) {

View File

@@ -1,39 +1,39 @@
import { paths } from '@/config'
import { Album, Artist, Mix, Page, Playlist } from '@/interfaces'
import { Album, Artist, Collection, Mix, Playlist } from '@/interfaces'
import { Notification, NotifType } from '@/stores/notification'
import useAxios from './useAxios'
const { base: basePageUrl } = paths.api.pages
const { base: baseCollectionUrl } = paths.api.collections
export async function getAllPages() {
export async function getAllCollections() {
const { data, status } = await useAxios({
url: basePageUrl,
url: baseCollectionUrl,
method: 'GET',
})
if (status == 200) {
return data as Page[]
return data as Collection[]
}
return []
}
export async function getPage(page_id: string) {
export async function getCollection(collection_id: string) {
const { data, status } = await useAxios({
url: basePageUrl + `/${page_id}`,
url: baseCollectionUrl + `/${collection_id}`,
method: 'GET',
})
return data as Page
return data as Collection
}
export async function createNewPage(
export async function createNewCollection(
name: string,
description: string,
items?: { hash: string; type: string; extra: any }[]
) {
const { data, status } = await useAxios({
url: basePageUrl,
url: baseCollectionUrl,
props: {
name,
description,
@@ -49,9 +49,9 @@ export async function createNewPage(
return false
}
export async function updatePage(page: Page, name: string, description: string) {
export async function updateCollection(collection: Collection, name: string, description: string) {
const { data, status } = await useAxios({
url: basePageUrl + `/${page.id}`,
url: baseCollectionUrl + `/${collection.id}`,
props: {
name,
description,
@@ -60,14 +60,14 @@ export async function updatePage(page: Page, name: string, description: string)
})
if (status == 200) {
return data.page as Page
return data as Collection
}
return null
}
export async function addOrRemoveItemFromPage(
page_number: number,
export async function addOrRemoveItemFromCollection(
collection_id: number,
item: Album | Artist | Mix | Playlist,
type: string,
command: 'add' | 'remove'
@@ -94,11 +94,11 @@ export async function addOrRemoveItemFromPage(
}
if (payload.hash === '') {
throw new Error('Invalid item type. Item not added to page.')
throw new Error('Invalid item type. Item not added to collection.')
}
const { data, status } = await useAxios({
url: basePageUrl + `/${page_number}/items`,
url: baseCollectionUrl + `/${collection_id}/items`,
props: {
item: payload,
},
@@ -116,7 +116,7 @@ export async function addOrRemoveItemFromPage(
}
if (status == 400) {
new Notification(`${payload.type[0].toUpperCase() + payload.type.slice(1)} already in page`, NotifType.Error)
new Notification(`${payload.type[0].toUpperCase() + payload.type.slice(1)} already in collection`, NotifType.Error)
return false
}
@@ -124,9 +124,9 @@ export async function addOrRemoveItemFromPage(
return false
}
export async function deletePage(page_number: number) {
export async function deleteCollection(collection_id: number) {
const { data, status } = await useAxios({
url: basePageUrl + `/${page_number}`,
url: baseCollectionUrl + `/${collection_id}`,
method: 'DELETE',
})

View File

@@ -32,6 +32,7 @@ export async function getBackups() {
playlists: number
scrobbles: number
favorites: number
collections: number
date: string
}

View File

@@ -1,25 +1,17 @@
import { FetchProps } from '@/interfaces'
import axios, { AxiosError, AxiosResponse } from 'axios'
import axios from 'axios'
import useModal from '@/stores/modal'
import useLoaderStore from '@/stores/loader'
import { logoutUser } from './auth'
const development = import.meta.env.DEV
export function getBaseUrl() {
const base_url = window.location.origin
if (!development) {
return base_url
}
const splits = base_url.split(':')
return base_url.replace(splits[splits.length - 1], '1980')
if (window.location.protocol === 'https:') {
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = 'upgrade-insecure-requests';
document.head.appendChild(meta);
}
axios.defaults.baseURL = getBaseUrl()
export default async (args: FetchProps, withCredentials: boolean = true) => {
const on_ngrok = args.url.includes('ngrok')
const ngrok_config = {
@@ -61,7 +53,7 @@ export default async (args: FetchProps, withCredentials: boolean = true) => {
try {
isSignatureError = error.response.data.msg == 'Signature verification failed'
} catch (error) {
console.log('Error:', error)
console.error('Error:', error)
}
if (error.response?.status === 422 && isSignatureError) {

View File

@@ -1,270 +1,269 @@
import { createRouter, createWebHashHistory, RouterOptions } from "vue-router";
import { createRouter, createWebHashHistory, RouterOptions } from 'vue-router'
import state from "@/composables/state";
import useAlbumPageStore from "@/stores/pages/album";
import useFolderPageStore from "@/stores/pages/folder";
import usePlaylistPageStore from "@/stores/pages/playlist";
import usePlaylistListPageStore from "@/stores/pages/playlists";
import useArtistPageStore from "@/stores/pages/artist";
import state from '@/composables/state'
import useAlbumPageStore from '@/stores/pages/album'
import useFolderPageStore from '@/stores/pages/folder'
import usePlaylistPageStore from '@/stores/pages/playlist'
import usePlaylistListPageStore from '@/stores/pages/playlists'
import useArtistPageStore from '@/stores/pages/artist'
import HomeView from "@/views/HomeView";
const Lyrics = () => import("@/views/LyricsView");
const ArtistView = () => import("@/views/ArtistView");
const NotFound = () => import("@/views/NotFound.vue");
const NowPlaying = () => import("@/views/NowPlaying");
const SearchView = () => import("@/views/SearchView");
const AlbumList = () => import("@/views/AlbumListView");
const FolderView = () => import("@/views/FolderView.vue");
const FavoritesView = () => import("@/views/Favorites.vue");
const SettingsView = () => import("@/views/SettingsView.vue");
const AlbumView = () => import("@/views/AlbumView/index.vue");
const ArtistTracksView = () => import("@/views/ArtistTracks.vue");
const PlaylistListView = () => import("@/views/PlaylistList.vue");
const FavoriteTracks = () => import("@/views/FavoriteTracks.vue");
const PlaylistView = () => import("@/views/PlaylistView/index.vue");
const ArtistDiscographyView = () => import("@/views/ArtistDiscography.vue");
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");
import HomeView from '@/views/HomeView'
const Lyrics = () => import('@/views/LyricsView')
const ArtistView = () => import('@/views/ArtistView')
const NotFound = () => import('@/views/NotFound.vue')
const NowPlaying = () => import('@/views/NowPlaying')
const SearchView = () => import('@/views/SearchView')
const AlbumList = () => import('@/views/AlbumListView')
const FolderView = () => import('@/views/FolderView.vue')
const FavoritesView = () => import('@/views/Favorites.vue')
const SettingsView = () => import('@/views/SettingsView.vue')
const AlbumView = () => import('@/views/AlbumView/index.vue')
const ArtistTracksView = () => import('@/views/ArtistTracks.vue')
const PlaylistListView = () => import('@/views/PlaylistList.vue')
const FavoriteTracks = () => import('@/views/FavoriteTracks.vue')
const PlaylistView = () => import('@/views/PlaylistView/index.vue')
const ArtistDiscographyView = () => import('@/views/ArtistDiscography.vue')
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 Collection = () => import('@/views/Collections/Collection.vue')
const folder = {
path: "/folder/:path",
name: "FolderView",
component: FolderView,
beforeEnter: async (to: any) => {
state.loading.value = true;
await useFolderPageStore()
.fetchAll(to.params.path, true)
.then(() => {
state.loading.value = false;
});
},
};
path: '/folder/:path',
name: 'FolderView',
component: FolderView,
beforeEnter: async (to: any) => {
state.loading.value = true
await useFolderPageStore()
.fetchAll(to.params.path, true)
.then(() => {
state.loading.value = false
})
},
}
const playlists = {
path: "/playlists",
name: "PlaylistList",
component: PlaylistListView,
beforeEnter: async () => {
state.loading.value = true;
await usePlaylistListPageStore()
.fetchAll()
.then(() => {
state.loading.value = false;
});
},
};
path: '/playlists',
name: 'PlaylistList',
component: PlaylistListView,
beforeEnter: async () => {
state.loading.value = true
await usePlaylistListPageStore()
.fetchAll()
.then(() => {
state.loading.value = false
})
},
}
const playlistView = {
path: "/playlist/:pid",
name: "PlaylistView",
component: PlaylistView,
beforeEnter: async (to: any) => {
state.loading.value = true;
await usePlaylistPageStore()
.fetchAll(to.params.pid)
.then(() => {
state.loading.value = false;
});
},
};
path: '/playlist/:pid',
name: 'PlaylistView',
component: PlaylistView,
beforeEnter: async (to: any) => {
state.loading.value = true
await usePlaylistPageStore()
.fetchAll(to.params.pid)
.then(() => {
state.loading.value = false
})
},
}
const albumView = {
path: "/albums/:albumhash",
name: "AlbumView",
component: AlbumView,
beforeEnter: async (to: any) => {
state.loading.value = true;
const store = useAlbumPageStore();
path: '/albums/:albumhash',
name: 'AlbumView',
component: AlbumView,
beforeEnter: async (to: any) => {
state.loading.value = true
const store = useAlbumPageStore()
await store.fetchTracksAndArtists(to.params.albumhash).then(() => {
state.loading.value = false;
});
},
};
await store.fetchTracksAndArtists(to.params.albumhash).then(() => {
state.loading.value = false
})
},
}
const artistView = {
path: "/artists/:hash",
name: "ArtistView",
component: ArtistView,
beforeEnter: async (to: any) => {
state.loading.value = true;
path: '/artists/:hash',
name: 'ArtistView',
component: ArtistView,
beforeEnter: async (to: any) => {
state.loading.value = true
await useArtistPageStore()
.getData(to.params.hash)
.then(() => {
state.loading.value = false;
});
},
};
await useArtistPageStore()
.getData(to.params.hash)
.then(() => {
state.loading.value = false
})
},
}
const NowPlayingView = {
path: "/nowplaying/:tab",
name: "NowPlaying",
component: NowPlaying,
};
path: '/nowplaying/:tab',
name: 'NowPlaying',
component: NowPlaying,
}
const LyricsView = {
path: "/lyrics",
name: "LyricsView",
component: Lyrics,
};
path: '/lyrics',
name: 'LyricsView',
component: Lyrics,
}
const ArtistTracks = {
path: "/artists/:hash/tracks",
name: "ArtistTracks",
component: ArtistTracksView,
};
path: '/artists/:hash/tracks',
name: 'ArtistTracks',
component: ArtistTracksView,
}
const artistDiscography = {
path: "/artists/:hash/discography/:type",
name: "ArtistDiscographyView",
component: ArtistDiscographyView,
};
path: '/artists/:hash/discography/:type',
name: 'ArtistDiscographyView',
component: ArtistDiscographyView,
}
const settings = {
path: "/settings/:tab",
name: "SettingsView",
component: SettingsView,
};
path: '/settings/:tab',
name: 'SettingsView',
component: SettingsView,
}
const search = {
path: "/search/:page",
name: "SearchView",
component: SearchView,
};
path: '/search/:page',
name: 'SearchView',
component: SearchView,
}
const favorites = {
path: "/favorites",
name: "FavoritesView",
component: FavoritesView,
};
path: '/favorites',
name: 'FavoritesView',
component: FavoritesView,
}
const favoriteAlbums = {
path: "/favorites/albums",
name: "FavoriteAlbums",
component: FavoriteCardScroller,
};
path: '/favorites/albums',
name: 'FavoriteAlbums',
component: FavoriteCardScroller,
}
const favoriteArtists = {
path: "/favorites/artists",
name: "FavoriteArtists",
component: FavoriteCardScroller,
};
path: '/favorites/artists',
name: 'FavoriteArtists',
component: FavoriteCardScroller,
}
const favoriteTracks = {
path: "/favorites/tracks",
name: "FavoriteTracks",
component: FavoriteTracks,
};
path: '/favorites/tracks',
name: 'FavoriteTracks',
component: FavoriteTracks,
}
const notFound = {
name: "NotFound",
path: "/:pathMatch(.*)",
component: NotFound,
};
name: 'NotFound',
path: '/:pathMatch(.*)',
component: NotFound,
}
const Home = {
path: "/",
name: "Home",
component: HomeView,
};
path: '/',
name: 'Home',
component: HomeView,
}
const AlbumListView = {
path: "/albums",
name: "AlbumListView",
component: AlbumList,
};
path: '/albums',
name: 'AlbumListView',
component: AlbumList,
}
const Stats = {
path: "/stats",
name: "StatsView",
component: StatsView,
};
path: '/stats',
name: 'StatsView',
component: StatsView,
}
const ArtistListView = {
...AlbumListView,
path: "/artists",
name: "ArtistListView",
};
...AlbumListView,
path: '/artists',
name: 'ArtistListView',
}
const Mix = {
path: "/mix/:mixid",
name: "MixView",
component: MixView,
};
path: '/mix/:mixid',
name: 'MixView',
component: MixView,
}
const MixList = {
path: "/mixes/:type",
name: "MixListView",
component: MixListView,
};
path: '/mixes/:type',
name: 'MixListView',
component: MixListView,
}
const PageView = {
path: "/pages/:page",
name: "Page",
component: Page,
};
path: '/collections/:collection',
name: 'Collection',
component: Collection,
}
const routes = [
folder,
playlists,
playlistView,
albumView,
artistView,
artistDiscography,
settings,
search,
notFound,
ArtistTracks,
favorites,
favoriteAlbums,
favoriteTracks,
favoriteArtists,
NowPlayingView,
Home,
AlbumListView,
ArtistListView,
LyricsView,
Stats,
Mix,
MixList,
PageView,
];
folder,
playlists,
playlistView,
albumView,
artistView,
artistDiscography,
settings,
search,
notFound,
ArtistTracks,
favorites,
favoriteAlbums,
favoriteTracks,
favoriteArtists,
NowPlayingView,
Home,
AlbumListView,
ArtistListView,
LyricsView,
Stats,
Mix,
MixList,
PageView,
]
const Routes = {
folder: folder.name,
playlists: playlists.name,
playlist: playlistView.name,
album: albumView.name,
artist: artistView.name,
artistDiscography: artistDiscography.name,
settings: settings.name,
search: search.name,
notFound: notFound.name,
artistTracks: ArtistTracks.name,
favorites: favorites.name,
favoriteAlbums: favoriteAlbums.name,
favoriteTracks: favoriteTracks.name,
favoriteArtists: favoriteArtists.name,
nowPlaying: NowPlayingView.name,
Home: Home.name,
AlbumList: AlbumListView.name,
ArtistList: ArtistListView.name,
Lyrics: LyricsView.name,
Stats: Stats.name,
Mix: Mix.name,
MixList: MixList.name,
Page: PageView.name,
};
folder: folder.name,
playlists: playlists.name,
playlist: playlistView.name,
album: albumView.name,
artist: artistView.name,
artistDiscography: artistDiscography.name,
settings: settings.name,
search: search.name,
notFound: notFound.name,
artistTracks: ArtistTracks.name,
favorites: favorites.name,
favoriteAlbums: favoriteAlbums.name,
favoriteTracks: favoriteTracks.name,
favoriteArtists: favoriteArtists.name,
nowPlaying: NowPlayingView.name,
Home: Home.name,
AlbumList: AlbumListView.name,
ArtistList: ArtistListView.name,
Lyrics: LyricsView.name,
Stats: Stats.name,
Mix: Mix.name,
MixList: MixList.name,
Page: PageView.name,
}
const router = createRouter({
mode: "hash",
history: createWebHashHistory(import.meta.env.BASE_URL),
routes,
} as RouterOptions);
mode: 'hash',
history: createWebHashHistory(import.meta.env.BASE_URL),
routes,
} as RouterOptions)
export { router, Routes };
export { router, Routes }

View File

@@ -50,7 +50,7 @@ export const library = {
show_if: loggedInUserIsAdmin,
groups: [
{
title: "Root directories",
title: "Folders",
icon: FolderSvg,
desc: rootRootStrings.desc,
settings: [...rootDirSettings],

View File

@@ -1,26 +1,41 @@
import { SettingType } from "../enums";
import { Setting } from "@/interfaces/settings";
import { SettingType } from '../enums'
import { Setting } from '@/interfaces/settings'
import useSettingsStore from "@/stores/settings";
import useSettingsStore from '@/stores/settings'
const settings = useSettingsStore;
const settings = useSettingsStore
const disable_np_img: Setting = {
title: "Hide album art from the left sidebar",
type: SettingType.binary,
state: () => !settings().use_np_img,
action: () => settings().toggleUseNPImg(),
show_if: () => !settings().is_alt_layout,
};
title: 'Hide album art from the left sidebar',
type: SettingType.binary,
state: () => !settings().use_np_img,
action: () => settings().toggleUseNPImg(),
show_if: () => !settings().is_alt_layout,
}
const showNowPlayingOnTabTitle: Setting = {
title: "Show Now Playing track on tab title",
desc: "Replace current page info with Now Playing track info",
type: SettingType.binary,
state: () => settings().nowPlayingTrackOnTabTitle,
action: () => settings().toggleNowPlayingTrackOnTabTitle(),
};
title: 'Show Now Playing track on tab title',
desc: 'Replace current page info with Now Playing track info',
type: SettingType.binary,
state: () => settings().nowPlayingTrackOnTabTitle,
action: () => settings().toggleNowPlayingTrackOnTabTitle(),
}
const showInlineFavIcon: Setting = {
title: 'Show inline favorite icon',
desc: 'Show the favorite button next to the track duration',
type: SettingType.binary,
state: () => settings().showInlineFavIcon,
action: () => settings().toggleShowInlineFavIcon(),
}
const highlightFavoriteTracks: Setting = {
title: 'Highlight favorite tracks',
desc: 'Always show the favorite button for favorited tracks',
type: SettingType.binary,
state: () => settings()._highlightFavoriteTracks,
action: () => settings().toggleHighlightFavoriteTracks(),
show_if: () => settings().showInlineFavIcon,
}
export default [disable_np_img, showNowPlayingOnTabTitle];
export default [disable_np_img, showNowPlayingOnTabTitle, showInlineFavIcon, highlightFavoriteTracks]

View File

@@ -4,7 +4,7 @@ import { SettingType } from '../enums'
import { manageRootDirsStrings as data } from '../strings'
import useModalStore from '@/stores/modal'
import useSettingsStore from '@/stores/settings'
import settings from '@/stores/settings'
const text = data.settings
@@ -12,7 +12,7 @@ const change_root_dirs: Setting = {
title: text.change,
type: SettingType.button,
state: null,
button_text: () => `\xa0 \xa0 ${useSettingsStore().root_dirs.length ? 'Modify' : 'Configure'} \xa0 \xa0`,
button_text: () => `\xa0 \xa0 ${settings().root_dirs.length ? 'Modify' : 'Configure'} \xa0 \xa0`,
action: () => useModalStore().showRootDirsPromptModal(),
}
@@ -20,11 +20,11 @@ const list_root_dirs: Setting = {
title: text.list_root_dirs,
type: SettingType.root_dirs,
state: () =>
useSettingsStore().root_dirs.map(d => ({
settings().root_dirs.map(d => ({
title: d,
action: () => {
editRootDirs([], [d]).then(all_dirs => {
useSettingsStore().setRootDirs(all_dirs)
settings().setRootDirs(all_dirs)
})
},
})),
@@ -32,6 +32,14 @@ const list_root_dirs: Setting = {
action: () => triggerScan(),
}
const show_playlists_in_folders: Setting = {
title: 'Show playlists in folder view',
desc: 'Browse playlists and favorites in folders screen (meant for mobile app)',
type: SettingType.binary,
state: () => settings().show_playlists_in_folders,
action: () => settings().toggleShowPlaylistsInFolders(),
}
// const enable_scans: Setting = {
// title: "Enable periodic scans",
// type: SettingType.binary,
@@ -57,5 +65,6 @@ const list_root_dirs: Setting = {
export default [
change_root_dirs,
list_root_dirs,
show_playlists_in_folders,
// useWatchdog, enable_scans, periodicScanInterval
]

View File

@@ -30,7 +30,7 @@ export default defineStore('newModal', {
showNewPlaylistModal(props: any = {}) {
this.showModal(ModalOptions.newPlaylist, props)
},
showPageModal(props: any = {}) {
showCollectionModal(props: any = {}) {
this.showModal(ModalOptions.page, props)
},
showSaveFolderAsPlaylistModal(path: string) {

View File

@@ -0,0 +1,30 @@
import { Album, Artist, Collection } from '@/interfaces'
import { getCollection } from '@/requests/collections'
import { defineStore } from 'pinia'
export default defineStore('collections', {
state: () => ({
collection: <Collection | null>null,
}),
actions: {
async fetchCollection(collection_id: string) {
this.collection = await getCollection(collection_id)
},
async removeLocalItem(item: Album | Artist, type: 'album' | 'artist') {
if (!this.collection) return
if (type == 'album') {
this.collection.items = this.collection.items.filter(i => {
return (i as Album).albumhash != (item as Album).albumhash
})
} else {
this.collection.items = this.collection.items.filter(i => {
return (i as Artist).artisthash != (item as Artist).artisthash
})
}
},
clearStore() {
this.collection = null
},
},
})

View File

@@ -1,27 +0,0 @@
import { Artist, Album, Page } from '@/interfaces'
import { getPage } from '@/requests/pages'
import { defineStore } from 'pinia'
export default defineStore('page', {
state: () => ({
page: <Page | null>null,
}),
actions: {
async fetchPage(page_no: string) {
this.page = await getPage(page_no)
},
async removeLocalItem(item: Album | Artist, type: 'album' | 'artist') {
if (!this.page) return
if (type == 'album') {
this.page.items = this.page.items.filter(i => {
return (i as Album).albumhash != (item as Album).albumhash
})
} else {
this.page.items = this.page.items.filter(i => {
return (i as Artist).artisthash != (item as Artist).artisthash
})
}
},
},
})

View File

@@ -10,7 +10,7 @@ import useTracklist from './queue/tracklist'
import useSettings from './settings'
import useTracker from './tracker'
import { paths } from '@/config'
import { getBaseUrl, paths } from '@/config'
import updateMediaNotif from '@/helpers/mediaNotification'
import { crossFade } from '@/utils/audio/crossFade'
@@ -81,15 +81,12 @@ class AudioSource {
this.playingSource.pause()
}
async playPlayingSource(
trackSilence?: { starting_file: number; ending_file: number }
) {
async playPlayingSource(trackSilence?: { starting_file: number; ending_file: number }) {
const trackDuration = trackSilence
? Math.floor(trackSilence.ending_file / 1000 - trackSilence.starting_file / 1000)
: null
if(this.requiredAPBlockBypass)
this.applyAPBlockBypass()
if (this.requiredAPBlockBypass) this.applyAPBlockBypass()
await this.playingSource.play().catch(this.handlers.onPlaybackError)
navigator.mediaSession.playbackState = 'playing'
@@ -110,11 +107,14 @@ class AudioSource {
*
* this workaround plays the `standbySource` along with the `playingSource` to meet the first condition.
*/
private applyAPBlockBypass(){
private applyAPBlockBypass() {
this.standbySource.src = ''
this.standbySource.play().then(() => {
this.standbySource.pause()
}).catch(() => {})
this.standbySource
.play()
.then(() => {
this.standbySource.pause()
})
.catch(() => {})
this.requiredAPBlockBypass = false
}
@@ -127,9 +127,11 @@ export function getUrl(filepath: string, trackhash: string, use_legacy: boolean)
use_legacy = true
const { streaming_container, streaming_quality } = useSettings()
return `${paths.api.files}/${trackhash + (use_legacy ? '/legacy' : '')}?filepath=${encodeURIComponent(
const url = `${paths.api.files}/${trackhash + (use_legacy ? '/legacy' : '')}?filepath=${encodeURIComponent(
filepath
)}&container=${streaming_container}&quality=${streaming_quality}`
return getBaseUrl() + url
}
const audioSource = new AudioSource()
@@ -228,9 +230,12 @@ export const usePlayer = defineStore('player', () => {
const handlePlayErrors = (e: Event | string) => {
if (e instanceof DOMException) {
if(e.name === 'NotAllowedError') {
if (e.name === 'NotAllowedError') {
queue.playPause()
return toast.showNotification('Tap anywhere in the page and try again (autoplay blocked)', NotifType.Error)
return toast.showNotification(
'Tap anywhere in the page and try again (autoplay blocked)',
NotifType.Error
)
}
return toast.showNotification('Player Error: ' + e.message, NotifType.Error)
@@ -260,9 +265,9 @@ export const usePlayer = defineStore('player', () => {
return lyrics.getLyrics()
}
if (!settings.use_lyrics_plugin) {
lyrics.checkExists(queue.currenttrack.filepath, queue.currenttrack.trackhash)
}
// if (!settings.use_lyrics_plugin) {
// lyrics.checkExists(queue.currenttrack.filepath, queue.currenttrack.trackhash)
// }
}
const onAudioCanPlay = () => {
@@ -278,12 +283,14 @@ export const usePlayer = defineStore('player', () => {
const { submitData } = tracker
submitData()
console.log('audio ended')
console.log(nextAudioData)
if (settings.repeat == 'none') {
queue.playPause()
queue.moveForward()
return
}
// INFO: if next audio is not loaded, manually move forward
if (nextAudioData.loaded === false) {
console.log('next audio not loaded')
clearNextAudioData()
queue.playNext()
}
@@ -343,6 +350,10 @@ export const usePlayer = defineStore('player', () => {
const silence = e.data
if (!silence.ending_file){
return
}
nextAudioData.silence.starting_file = silence.starting_file
currentAudioData.silence.ending_file = silence.ending_file
nextAudioData.loaded = silence !== null
@@ -378,7 +389,7 @@ export const usePlayer = defineStore('player', () => {
currentAudioData.silence = nextAudioData.silence
currentAudioData.filepath = nextAudioData.filepath
maxSeekPercent.value = 0
audioSource.playPlayingSource(nextAudioData.silence);
audioSource.playPlayingSource(nextAudioData.silence)
clearNextAudioData()
queue.moveForward()
@@ -389,10 +400,10 @@ export const usePlayer = defineStore('player', () => {
const initLoadingNextTrackAudio = () => {
const { currentindex } = queue
const { length } = tracklist
const { repeat_all, repeat_one } = settings
const { repeat } = settings
// if no repeat && is last track, return
if (currentindex === length - 1 && !repeat_all && !repeat_one) {
if (currentindex === length - 1 && repeat == 'none') {
return
}

View File

@@ -76,12 +76,12 @@ export default defineStore('Queue', {
const { tracklist } = useTracklist()
const is_last = this.currentindex === tracklist.length - 1
if (settings.repeat_one) {
if (settings.repeat == 'one') {
this.play(this.currentindex, false)
return
}
if (settings.repeat_all) {
if (settings.repeat == 'all') {
this.play(is_last ? 0 : this.currentindex + 1, false)
return
}
@@ -189,9 +189,9 @@ export default defineStore('Queue', {
},
previndex(): number {
const { tracklist } = useTracklist()
const { repeat_one } = useSettings()
const { repeat } = useSettings()
if (repeat_one) {
if (repeat == 'one') {
return this.currentindex
}
@@ -199,9 +199,9 @@ export default defineStore('Queue', {
},
nextindex(): number {
const { tracklist } = useTracklist()
const { repeat_one } = useSettings()
const { repeat } = useSettings()
if (repeat_one) {
if (repeat == 'one') {
return this.currentindex
}

View File

@@ -7,25 +7,9 @@ import useQueue from '@/stores/queue'
import useSettings from '@/stores/settings'
import { FromOptions } from '@/enums'
import {
fromAlbum,
fromArtist,
fromFav,
fromFolder,
fromMix,
fromPlaylist,
fromSearch,
Track,
} from '@/interfaces'
import { fromAlbum, fromArtist, fromFav, fromFolder, fromMix, fromPlaylist, fromSearch, Track } from '@/interfaces'
export type From =
| fromFolder
| fromAlbum
| fromPlaylist
| fromSearch
| fromArtist
| fromFav
| fromMix
export type From = fromFolder | fromAlbum | fromPlaylist | fromSearch | fromArtist | fromFav | fromMix
function shuffle(tracks: Track[]) {
const shuffled = tracks.slice()
@@ -56,12 +40,6 @@ export default defineStore('tracklist', {
this.tracklist.push(...tracklist)
}
const settings = useSettings()
if (settings.repeat_one) {
settings.toggleRepeatMode()
}
const { focusCurrentInSidebar } = useInterface()
focusCurrentInSidebar(1000)
usePlayer().clearNextAudio()
@@ -95,7 +73,13 @@ export default defineStore('tracklist', {
this.setNewList(tracks)
},
setFromMix(name: string, id: string, tracks: Track[], sourcehash: string, image: { type: 'mix' | 'track', image: string }) {
setFromMix(
name: string,
id: string,
tracks: Track[],
sourcehash: string,
image: { type: 'mix' | 'track'; image: string }
) {
this.from = <fromMix>{
type: FromOptions.mix,
name: name,
@@ -137,10 +121,7 @@ export default defineStore('tracklist', {
this.insertAt(tracks, this.tracklist.length)
const Toast = useToast()
Toast.showNotification(
`Added ${tracks.length} tracks to queue`,
NotifType.Success
)
Toast.showNotification(`Added ${tracks.length} tracks to queue`, NotifType.Success)
},
insertAt(tracks: Track[], index: number) {
this.tracklist.splice(index, 0, ...tracks)
@@ -160,14 +141,7 @@ export default defineStore('tracklist', {
this.tracklist = shuffle(this.tracklist)
},
removeByIndex(index: number) {
const {
currentindex,
nextindex,
playing,
playNext,
moveForward,
setCurrentIndex,
} = useQueue()
const { currentindex, nextindex, playing, playNext, moveForward, setCurrentIndex } = useQueue()
const player = usePlayer()
if (this.tracklist.length == 1) {
@@ -207,10 +181,7 @@ export default defineStore('tracklist', {
this.tracklist.splice(currentindex + 1, 0, ...tracks)
const Toast = useToast()
Toast.showNotification(
`Added ${tracks.length} tracks to queue`,
NotifType.Success
)
Toast.showNotification(`Added ${tracks.length} tracks to queue`, NotifType.Success)
},
},
getters: {

View File

@@ -24,10 +24,7 @@ export default defineStore('search', () => {
const currentTab = ref('top')
const top_results = reactive({
query: '',
top_result: {
type: <null | string>null,
item: <Track | Album | Artist>{},
},
top_result: <Track | Album | Artist>{},
tracks: <Track[]>[],
albums: <Album[]>[],
artists: <Artist[]>[],

View File

@@ -10,6 +10,7 @@ import { content_width } from '../content-width'
import { getLastFmApiSig } from '@/context_menus/hashing'
import useAxios from '@/requests/useAxios'
import { paths } from '@/config'
import { router, Routes } from '@/router'
export default defineStore('settings', {
state: () => ({
@@ -17,8 +18,9 @@ export default defineStore('settings', {
extend_width: false,
contextChildrenShowMode: contextChildrenShowMode.hover,
artist_top_tracks_count: 5,
repeat_all: true,
repeat_one: false,
// repeat_all: true,
// repeat_one: false,
repeat: <'all' | 'one' | 'none'>'all',
root_dir_set: false,
root_dirs: <string[]>[],
@@ -37,6 +39,7 @@ export default defineStore('settings', {
merge_albums: false,
show_albums_as_singles: false,
separators: <string[]>[],
show_playlists_in_folders: false,
// client
useCircularArtistImg: true,
@@ -59,7 +62,7 @@ export default defineStore('settings', {
// audio
use_silence_skip: true,
use_crossfade: false,
crossfade_duration: 2000, // milliseconds
crossfade_duration: 1000, // milliseconds
use_legacy_streaming_endpoint: false,
// layout
@@ -71,6 +74,8 @@ export default defineStore('settings', {
// stats
statsgroup: 'artists',
statsperiod: 'week',
showInlineFavIcon: false,
_highlightFavoriteTracks: false,
}),
actions: {
mapDbSettings(settings: DBSettings) {
@@ -83,6 +88,7 @@ export default defineStore('settings', {
this.merge_albums = settings.mergeAlbums
this.separators = settings.artistSeparators
this.show_albums_as_singles = settings.showAlbumsAsSingles
this.show_playlists_in_folders = settings.showPlaylistsInFolderView
this.enablePeriodicScans = settings.enablePeriodicScans
this.periodicInterval = settings.scanInterval
@@ -104,6 +110,12 @@ export default defineStore('settings', {
toggleUseNPImg() {
this.use_np_img = !this.use_np_img
},
toggleShowInlineFavIcon() {
this.showInlineFavIcon = !this.showInlineFavIcon
},
toggleHighlightFavoriteTracks() {
this._highlightFavoriteTracks = !this._highlightFavoriteTracks
},
// sidebar 👇
toggleDisableSidebar() {
if (this.is_alt_layout) {
@@ -128,20 +140,18 @@ export default defineStore('settings', {
},
// repeat 👇
toggleRepeatMode() {
if (this.repeat_all) {
this.repeat_all = false
this.repeat_one = true
if (this.repeat == 'all') {
this.repeat = 'one'
return
}
if (this.repeat_one) {
this.repeat_one = false
this.repeat_all = false
if (this.repeat == 'one') {
this.repeat = 'none'
return
}
if (!this.repeat_all && !this.repeat_one) {
this.repeat_all = true
if (this.repeat == 'none') {
this.repeat = 'all'
}
},
setRootDirs(dirs: string[]) {
@@ -293,6 +303,10 @@ export default defineStore('settings', {
'show_albums_as_singles'
)
},
async toggleShowPlaylistsInFolders() {
return await this.genericToggleSetting('showPlaylistsInFolderView', !this.show_playlists_in_folders, 'show_playlists_in_folders'
)
},
async setLastfmApiKey(key: string) {
return await this.genericToggleSetting('lastfmApiKey', key, 'lastfm_api_key')
},
@@ -313,7 +327,6 @@ export default defineStore('settings', {
},
false
)
console.log('res: ', data)
if (data.status !== 200) {
return
@@ -333,8 +346,6 @@ export default defineStore('settings', {
},
})
console.log('res: ', res)
if (res.status !== 200) {
return
}
@@ -368,9 +379,6 @@ export default defineStore('settings', {
can_extend_width(): boolean {
return this.is_default_layout && xxl.value
},
no_repeat(): boolean {
return !this.repeat_all && !this.repeat_one
},
crossfade_duration_seconds(): number {
return this.crossfade_duration / 1000
},
@@ -379,6 +387,12 @@ export default defineStore('settings', {
},
is_default_layout: state => state.layout === '',
is_alt_layout: state => state.layout === 'alternate' && content_width.value > 900,
highlightFavoriteTracks(): boolean {
return (
!router.currentRoute.value.name?.toString().toLowerCase().startsWith('favorite') &&
this._highlightFavoriteTracks
)
},
},
persist: {
afterRestore: context => {

View File

@@ -156,7 +156,6 @@ function getArtistAlbumComponents(): ScrollerItem[] {
function getAlbumVersionsComponent(): ScrollerItem | null {
if (album.otherVersions.length == 0) return null
console.log(album.otherVersions)
return {
id: 'otherVersions',
component: CardScroller,

View File

@@ -1,14 +1,14 @@
<template>
<CardGridPage :items="page.page?.items || []">
<CardGridPage :items="collection.collection?.items || []">
<template #header>
<GenericHeader>
<GenericHeader v-if="collection.collection?.id">
<template #name>
<span @click="updatePage">
{{ page.page?.name }} <span><PencilSvg height="0.8rem" width="0.8rem" /></span
{{ collection.collection?.name }} <span><PencilSvg height="0.8rem" width="0.8rem" /></span
></span>
</template>
<template #description v-if="page.page?.extra.description">
<span @click="updatePage"> {{ page.page?.extra.description }} </span>
<template #description v-if="collection.collection?.extra.description">
<span @click="updatePage"> {{ collection.collection?.extra.description }} </span>
</template>
<template #right>
<button @click="deletePage"><DeleteSvg height="1.2rem" width="1.2rem" /> Delete</button>
@@ -19,7 +19,7 @@
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { onBeforeUnmount, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import DeleteSvg from '@/assets/icons/delete.svg'
@@ -28,30 +28,33 @@ import GenericHeader from '@/components/shared/GenericHeader.vue'
import CardGridPage from '@/views/SearchView/CardGridPage.vue'
import useModal from '@/stores/modal'
import usePage from '@/stores/pages/page'
import useCollection from '@/stores/pages/collections'
const modal = useModal()
const page = usePage()
const collection = useCollection()
onMounted(async () => {
const route = useRoute()
const page_id = route.params.page as string
page.fetchPage(page_id)
const collection_id = route.params.collection as string
collection.fetchCollection(collection_id)
})
function updatePage() {
console.log('update page')
modal.showPageModal({
page: page.page,
modal.showCollectionModal({
collection: collection.collection,
})
}
function deletePage() {
modal.showPageModal({
page: page.page,
modal.showCollectionModal({
collection: collection.collection,
delete: true,
})
}
onBeforeUnmount(() => {
collection.clearStore()
})
</script>
<style scoped lang="scss">

View File

@@ -1,128 +1,128 @@
<template>
<div class="content-page favorites-page">
<GenericHeader>
<template #name>Favorites</template>
<template #description
>{{ count.tracks }} Tracks {{ count.albums }} Albums {{ count.artists }} Artists</template
>
</GenericHeader>
<CardScroller
v-if="recentFavs.length"
class="recent-favs"
:items="recentFavs"
:title="'Recent'"
:play-source="playSources.favorite"
/>
<div v-if="favTracks.length" class="fav-tracks">
<TopTracks
:tracks="favTracks"
:route="'/favorites/tracks'"
:title="'Tracks'"
:play-handler="handlePlay"
:source="dropSources.favorite"
:total="count.tracks"
/>
<div class="content-page favorites-page">
<GenericHeader>
<template #name>Favorites</template>
<template #description
>{{ count.tracks }} Tracks {{ count.albums }} Albums {{ count.artists }} Artists</template
>
</GenericHeader>
<CardScroller
v-if="recentFavs.length"
class="recent-favs"
:items="recentFavs"
:title="'Recent'"
:play-source="playSources.favorite"
/>
<div v-if="favTracks.length" class="fav-tracks">
<TopTracks
:tracks="favTracks"
:route="'/favorites/tracks'"
:title="'Tracks'"
:play-handler="handlePlay"
:source="dropSources.favorite"
:total="count.tracks"
/>
</div>
<br />
<CardScroller
v-if="favAlbums.length"
:items="favAlbums.map(i => ({ type: 'album', item: i }))"
:title="'Albums'"
:route="'/favorites/albums'"
/>
<CardScroller
v-if="favArtists.length"
:items="favArtists.map(i => ({ type: 'artist', item: i }))"
:title="'Artists'"
:route="'/favorites/artists'"
/>
<NoItems :flag="noFavs" :icon="HeartSvg" :title="'No favorites found'" :description="description" />
</div>
<br />
<CardScroller
v-if="favAlbums.length"
:items="favAlbums.map((i) => ({ type: 'album', item: i }))"
:title="'Albums'"
:route="'/favorites/albums'"
/>
<CardScroller
v-if="favArtists.length"
:items="favArtists.map((i) => ({ type: 'artist', item: i }))"
:title="'Artists'"
:route="'/favorites/artists'"
/>
<NoItems :flag="noFavs" :icon="HeartSvg" :title="'No favorites found'" :description="description" />
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, Ref, ref } from "vue";
import { nextTick, onMounted, Ref, ref } from 'vue'
import { maxAbumCards, updateCardWidth } from "@/stores/content-width";
import { maxAbumCards, updateCardWidth } from '@/stores/content-width'
import { dropSources, playSources } from "@/enums";
import { playFromFavorites } from "@/helpers/usePlayFrom";
import { Album, Artist, RecentFavResult, Track } from "@/interfaces";
import { getAllFavs } from "@/requests/favorite";
import updatePageTitle from "@/utils/updatePageTitle";
import { dropSources, playSources } from '@/enums'
import { playFromFavorites } from '@/helpers/usePlayFrom'
import { Album, Artist, RecentFavResult, Track } from '@/interfaces'
import { getAllFavs } from '@/requests/favorite'
import updatePageTitle from '@/utils/updatePageTitle'
import HeartSvg from "@/assets/icons/heart-no-color.svg";
import TopTracks from "@/components/ArtistView/TopTracks.vue";
import CardScroller from "@/components/shared/CardScroller.vue";
import GenericHeader from "@/components/shared/GenericHeader.vue";
import NoItems from "@/components/shared/NoItems.vue";
import HeartSvg from '@/assets/icons/heart-no-color.svg'
import TopTracks from '@/components/ArtistView/TopTracks.vue'
import CardScroller from '@/components/shared/CardScroller.vue'
import GenericHeader from '@/components/shared/GenericHeader.vue'
import NoItems from '@/components/shared/NoItems.vue'
const description = `You can add tracks, albums and artists to your favorites by clicking the heart icon`;
const description = `You can add tracks, albums and artists to your favorites by clicking the heart icon`
const recentFavs: Ref<RecentFavResult[]> = ref([]);
const favAlbums: Ref<Album[]> = ref([]);
const favTracks: Ref<Track[]> = ref([]);
const favArtists: Ref<Artist[]> = ref([]);
const recentFavs: Ref<RecentFavResult[]> = ref([])
const favAlbums: Ref<Album[]> = ref([])
const favTracks: Ref<Track[]> = ref([])
const favArtists: Ref<Artist[]> = ref([])
const count = ref({
albums: 0,
tracks: 0,
artists: 0,
});
const noFavs = ref(false);
albums: 0,
tracks: 0,
artists: 0,
})
const noFavs = ref(false)
onMounted(() => {
updatePageTitle("Favorites");
const max = maxAbumCards.value;
updatePageTitle('Favorites')
const max = maxAbumCards.value
getAllFavs(6, max, max)
.then((favs) => {
recentFavs.value = favs.recents;
favAlbums.value = favs.albums;
favTracks.value = favs.tracks;
favArtists.value = favs.artists;
count.value = favs.count;
})
.then(() => {
noFavs.value = !favAlbums.value.length && !favTracks.value.length && !favArtists.value.length;
})
.then(async () => {
await nextTick();
updateCardWidth();
});
});
getAllFavs(6, max, max)
.then(favs => {
recentFavs.value = favs.recents
favAlbums.value = favs.albums
favTracks.value = favs.tracks
favArtists.value = favs.artists
count.value = favs.count
})
.then(() => {
noFavs.value = !favAlbums.value.length && !favTracks.value.length && !favArtists.value.length
})
.then(async () => {
await nextTick()
updateCardWidth()
})
})
function handlePlay(index: number) {
const track = favTracks.value[index];
if (!track) return;
playFromFavorites(track);
const track = favTracks.value[index]
if (!track) return
playFromFavorites(track)
}
</script>
<style lang="scss">
.favorites-page {
height: 100%;
overflow: auto;
height: 100%;
overflow: auto;
.recent-favs {
padding-top: 1rem;
}
.nothing h3 {
margin-top: 3rem;
}
.fav-tracks {
h3 {
margin-top: 0;
.recent-favs {
padding-top: 1rem;
}
.artist-top-tracks {
h3 {
padding-right: $small;
}
.nothing h3 {
margin-top: 3rem;
}
.fav-tracks {
h3 {
margin-top: 0;
}
.artist-top-tracks {
h3 {
padding-right: $small;
}
}
}
}
}
</style>

View File

@@ -8,6 +8,7 @@
<PageItem
v-for="item in home.homepageItems"
:key="item.path"
:title="item.title || ''"
:description="item.description"
:items="item.items"

View File

@@ -69,6 +69,7 @@ const scrollerItems = computed(() => {
props: {
items: props.items.slice(i * maxCards, (i + 1) * maxCards),
},
key: i,
})
}

View File

@@ -62,7 +62,6 @@ const scrollerItems = computed(() => {
}))
if (search.tracks.more) {
console.log('more tracks')
items.push({
// set to random to force re-render
id: Math.random(),