use third-party module to auto-persist queue store

+ more redesign
+ convert js files to ts
This commit is contained in:
geoffrey45 2022-08-19 15:58:32 +03:00
parent 5476575d10
commit 03219166c5
20 changed files with 305 additions and 197 deletions

View File

@ -16,6 +16,6 @@
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -4,11 +4,13 @@
<Notification />
<div id="app-grid">
<div class="l-sidebar rounded">
<Logo />
<Navigation />
<div class="l-album-art">
<nowPlaying />
<div class="withlogo">
<Logo />
<Navigation />
</div>
<nowPlaying />
<!-- <Playlists /> -->
</div>
<NavBar />
<div id="acontent" class="rounded">
@ -44,6 +46,7 @@ import SearchInput from "@/components/RightSideBar/SearchInput.vue";
import BottomBar from "@/components/BottomBar/BottomBar.vue";
import { readLocalStorage, writeLocalStorage } from "@/utils";
import Playlists from "./components/LeftSidebar/Playlists.vue";
const queue = useQStore();
const router = useRouter();
@ -94,6 +97,11 @@ onMounted(() => {
.l-sidebar {
position: relative;
.withlogo {
padding: 1rem;
}
.l-album-art {
width: calc(100% - 2rem);
position: absolute;

View File

@ -62,10 +62,12 @@
}
.l-sidebar {
width: 17rem;
width: 15rem;
grid-area: l-sidebar;
display: grid;
grid-template-rows: 1fr max-content;
gap: 1rem;
background-color: $black;
padding: 1rem;
}
.b-bar {

View File

@ -45,7 +45,7 @@
.bg-black {
background-color: $gray4;
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1);
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.425)
}
.heading {

View File

@ -4,11 +4,15 @@
<img
:src="paths.images.thumb + queue.currenttrack?.image"
alt=""
class="rounded"
class="rounded shadow-lg"
/>
<div class="tags">
<div class="np-artist ellip">
<span v-for="artist in putCommas(queue.currenttrack?.artists || ['Artist'])">
<span
v-for="artist in putCommas(
queue.currenttrack?.artists || ['Artist']
)"
>
{{ artist }}
</span>
</div>
@ -18,16 +22,27 @@
</div>
</div>
<Progress />
<!-- <div class="ex-hotkeys">
<HotKeys />
</div> -->
<div class="time">
<span class="current">{{ formatSeconds(queue.currentTime) }}</span>
<HotKeys />
<span class="full">{{
formatSeconds(queue.fullTime || queue.currenttrack.length)
}}</span>
</div>
</div>
</template>
<script setup lang="ts">
import "@/assets/scss/BottomBar/BottomBar.scss";
import { formatSeconds, putCommas } from "@/utils";
import HotKeys from "../LeftSidebar/NP/HotKeys.vue";
import Progress from "../LeftSidebar/NP/Progress.vue";
import useQStore from "@/stores/queue";
import { paths } from "@/config";
import useQStore from "@/stores/queue";
const queue = useQStore();
</script>
@ -36,10 +51,28 @@ const queue = useQStore();
.b-bar {
display: grid;
grid-template-rows: 1fr max-content;
border-radius: 1rem;
gap: 1rem;
padding: 1rem;
padding-bottom: 2rem;
padding-bottom: 1rem;
position: relative;
.time {
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
.full {
text-align: end;
}
}
.ex-hotkeys {
position: absolute;
width: 10rem;
right: 1rem;
top: 1rem;
border-radius: $small;
}
.info {
display: grid;
@ -55,11 +88,11 @@ const queue = useQStore();
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: $smaller;
.np-title {
font-size: 1.15rem;
font-weight: bold;
margin-bottom: $small;
}
.np-artist {

View File

@ -1,6 +1,6 @@
<template>
<div class="hotkeys">
<div class="hotkeys rounded noscroll">
<div class="image ctrl-btn" id="previous" @click="q.playPrev"></div>
<div
class="image ctrl-btn play-pause"
@ -28,13 +28,13 @@ const q = useQStore();
justify-content: center;
place-content: flex-end;
width: 100%;
background-color: $gray2;
.ctrl-btn {
height: 2.5rem;
width: 100%;
background-size: 1.5rem !important;
cursor: pointer;
border-radius: 0.5rem;
&:hover {
background-color: $accent;

View File

@ -1,47 +1,44 @@
<template>
<div class="info">
<div class="desc">
<div>
<router-link
:to="{
name: 'AlbumView',
params: {
hash: track?.albumhash ? track.albumhash : ' ',
},
}"
>
<div class="art">
<img
:src="imguri + track?.image"
alt=""
class="l-image rounded force-lm"
loading="lazy"
/>
</div>
</router-link>
<div class="sidebar-songcard">
<router-link
:to="{
name: 'AlbumView',
params: {
hash: track?.albumhash ? track.albumhash : ' ',
},
}"
>
<div class="art">
<img
:src="imguri + track?.image"
alt=""
class="l-image rounded force-lm"
loading="lazy"
/>
<div id="bitrate" v-if="track?.bitrate">
<span v-if="track.bitrate > 1500">MASTER</span>
<span v-else-if="track.bitrate > 330">FLAC</span>
<span v-else>MP3</span>
{{ track.bitrate }}
</div>
<div class="title ellip">{{ props.track?.title }}</div>
<div class="separator no-border"></div>
<div
class="artists ellip"
v-if="track?.artists && track?.artists[0] !== ''"
>
<span v-for="artist in putCommas(track.artists)" :key="artist">{{
artist
}}</span>
</div>
<div class="artists" v-else-if="track?.artists">
<span>{{ track.albumartist }}</span>
</div>
<div class="artists" v-else>
<span>Meh</span>
</div>
</div>
</router-link>
<div class="bottom">
<div class="title ellip">{{ props.track?.title }}</div>
<div
class="artists ellip"
v-if="track?.artists && track?.artists[0] !== ''"
>
<span v-for="artist in putCommas(track.artists)" :key="artist">{{
artist
}}</span>
</div>
<div class="artists" v-else-if="track?.artists">
<span>{{ track.albumartist }}</span>
</div>
<div class="artists" v-else>
<span>Meh</span>
</div>
</div>
</div>
@ -57,3 +54,53 @@ const props = defineProps<{
track: Track | null;
}>();
</script>
<style lang="scss">
.sidebar-songcard {
.art {
width: 100%;
aspect-ratio: 1;
place-items: center;
margin-bottom: $small;
position: relative;
img {
width: 100%;
height: auto;
aspect-ratio: 1;
object-fit: cover;
}
#bitrate {
position: absolute;
font-size: 0.75rem;
width: max-content;
padding: 0.2rem 0.35rem;
bottom: 1rem;
left: 1rem;
background-color: $black;
border-radius: $smaller;
box-shadow: 0rem 0rem 1rem rgba(0, 0, 0, 0.438);
}
}
.bottom {
display: grid;
gap: $smaller;
}
.title {
font-weight: 900;
word-break: break-all;
}
.artists {
font-size: 0.85rem;
opacity: 0.75;
&:hover {
text-decoration: underline 1px !important;
}
}
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<div class="sidebar-playlists">
<div class="header">your playlists</div>
<div class="list rounded">
<div v-for="p in pStore.playlists" class="ellip">
<router-link
:to="{
name: 'PlaylistView',
params: {
pid: p.playlistid,
},
}"
>
{{ p.name }}
</router-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import usePStore from "@/stores/pages/playlists";
import { onMounted } from "vue";
const pStore = usePStore();
onMounted(() => {
if (pStore.playlists.length == 0) {
pStore.fetchAll();
}
});
</script>
<style lang="scss">
.sidebar-playlists {
// outline: solid 1px;
display: grid;
grid-template-rows: max-content 1fr;
.header {
opacity: 0.5;
margin-bottom: $small;
margin-left: 1rem;
font-size: small;
}
.list {
padding: $small;
& > * {
padding: $small;
}
}
}
</style>

View File

@ -1,14 +1,5 @@
<template>
<div class="now-playing-card t-center rounded">
<div class="headin">Now playing</div>
<div
class="button menu rounded"
@click="showContextMenu"
:class="{ context_on: context_on }"
>
<MenuSvg />
</div>
<div class="separator no-border"></div>
<div>
<SongCard :track="queue.currenttrack" />
<div class="l-track-time">
@ -16,8 +7,8 @@
><span class="rounded">{{ formatSeconds(queue.duration.full) }}</span>
</div>
<Progress />
<HotKeys />
</div>
<HotKeys />
</div>
</template>
@ -65,10 +56,11 @@ const showContextMenu = (e: Event) => {
<style lang="scss">
.now-playing-card {
padding: 1rem;
background-color: $primary;
width: 100%;
display: grid;
grid-template-rows: 1fr max-content;
position: relative;
gap: 1rem;
.l-track-time {
display: flex;
@ -96,24 +88,6 @@ const showContextMenu = (e: Event) => {
}
}
.headin {
font-weight: bold;
font-size: 0.9rem;
}
.button {
position: absolute;
top: $small;
cursor: pointer;
transition: all 200ms;
display: flex;
align-items: center;
padding: $smaller;
&:hover {
background-color: $accent;
}
}
.context_on {
background-color: $accent;
@ -123,43 +97,5 @@ const showContextMenu = (e: Event) => {
right: $small;
transform: rotate(90deg);
}
.art {
width: 100%;
aspect-ratio: 1;
place-items: center;
margin-bottom: $small;
.l-image {
height: 100%;
width: 100%;
}
}
#bitrate {
position: absolute;
font-size: 0.75rem;
width: max-content;
padding: 0.2rem 0.35rem;
top: 14rem;
left: 2rem;
background-color: $black;
border-radius: $smaller;
box-shadow: 0rem 0rem 1rem rgba(0, 0, 0, 0.438);
}
.title {
font-weight: 900;
word-break: break-all;
}
.artists {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.808);
&:hover {
text-decoration: underline 1px !important;
}
}
}
</style>

View File

@ -16,7 +16,6 @@
#logo {
height: 4.5rem !important;
width: 15rem;
background-image: url(./../assets/images/logo.webp);
background-size: contain;
@include ximage;

View File

@ -1,7 +1,6 @@
<template>
<div class="up-next">
<div class="r-grid">
<UpNext :track="queue.tracklist[queue.next]" :playNext="queue.playNext" />
<div class="scrollable-r bg-black rounded">
<QueueActions />
<div
@ -23,7 +22,6 @@
</TransitionGroup>
</div>
</div>
<!-- <PlayingFrom :from="queue.from" /> -->
</div>
</div>
</template>
@ -35,9 +33,7 @@ import useQStore from "@/stores/queue";
import { focusElem } from "@/utils";
import TrackItem from "../shared/TrackItem.vue";
import PlayingFrom from "./Queue/playingFrom.vue";
import QueueActions from "./Queue/QueueActions.vue";
import UpNext from "./Queue/upNext.vue";
const queue = useQStore();
const mouseover = ref(false);
@ -66,8 +62,6 @@ onUpdated(() => {
opacity: 0;
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.queuelist-leave-active {
transition: none;
position: absolute;
@ -85,10 +79,9 @@ onUpdated(() => {
.r-grid {
position: relative;
height: 100%;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: max-content 1fr;
gap: 1rem;
// display: grid;
// gap: 1rem;
.scrollable-r {
height: 100%;

View File

@ -2,13 +2,16 @@ import "./assets/scss/index.scss";
import { createPinia } from "pinia";
import { createApp } from "vue";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(createPinia());
app.use(pinia);
app.use(router);
app.mount("#app");

View File

@ -1,5 +1,3 @@
// @ts-strict
import { defineStore } from "pinia";
import state from "../composables/state";
import { NotifType, useNotifStore } from "./notification";
@ -14,29 +12,6 @@ import {
Track,
} from "../interfaces";
function writeQueue(from: From, tracks: Track[]) {
localStorage.setItem(
"queue",
JSON.stringify({
from: from,
tracks: tracks,
})
);
}
function writeCurrent(index: number) {
localStorage.setItem("current", JSON.stringify(index));
}
function readCurrent(): number {
const current = localStorage.getItem("current");
if (current) {
return JSON.parse(current);
}
return 0;
}
function shuffle(tracks: Track[]) {
const shuffled = tracks.slice();
for (let i = shuffled.length - 1; i > 0; i--) {
@ -48,19 +23,15 @@ function shuffle(tracks: Track[]) {
type From = fromFolder | fromAlbum | fromPlaylist | fromSearch;
let audio = new Audio();
export default defineStore("Queue", {
state: () => ({
progressElem: HTMLElement,
audio: new Audio(),
duration: {
current: 0,
full: 0,
},
indexes: {
current: 0,
next: 0,
previous: 0,
},
current: 0,
next: 0,
prev: 0,
@ -81,25 +52,24 @@ export default defineStore("Queue", {
this.updateCurrent(index);
new Promise((resolve, reject) => {
this.audio.autoplay = true;
this.audio.src = uri;
this.audio.oncanplaythrough = resolve;
this.audio.onerror = reject;
audio.autoplay = true;
audio.src = uri;
audio.oncanplaythrough = resolve;
audio.onerror = reject;
})
.then(() => {
this.duration.full = this.audio.duration;
this.audio.play().then(() => {
this.duration.full = audio.duration;
audio.play().then(() => {
this.playing = true;
notif(track, this.playPause, this.playNext, this.playPrev);
this.audio.ontimeupdate = () => {
this.duration.current = this.audio.currentTime;
const bg_size =
(this.audio.currentTime / this.audio.duration) * 100;
audio.ontimeupdate = () => {
this.duration.current = audio.currentTime;
const bg_size = (audio.currentTime / audio.duration) * 100;
elem.style.backgroundSize = `${bg_size}% 100%`;
};
this.audio.onended = () => {
audio.onended = () => {
this.playNext();
};
});
@ -119,13 +89,13 @@ export default defineStore("Queue", {
});
},
playPause() {
if (this.audio.src === "") {
if (audio.src === "") {
this.play(this.current);
} else if (this.audio.paused) {
this.audio.play();
} else if (audio.paused) {
audio.play();
this.playing = true;
} else {
this.audio.pause();
audio.pause();
this.playing = false;
}
},
@ -137,7 +107,7 @@ export default defineStore("Queue", {
},
seek(pos: number) {
try {
this.audio.currentTime = pos;
audio.currentTime = pos;
} catch (error) {
if (error instanceof TypeError) {
console.error("Seek error: no audio");
@ -152,15 +122,11 @@ export default defineStore("Queue", {
this.from = parsed.from;
this.tracklist = parsed.tracks;
}
this.updateCurrent(readCurrent());
},
updateCurrent(index: number) {
this.setCurrent(index);
this.updateNext(index);
this.updatePrev(index);
writeCurrent(index);
},
updateNext(index: number) {
if (index == this.tracklist.length - 1) {
@ -190,7 +156,6 @@ export default defineStore("Queue", {
if (this.tracklist !== tracklist) {
this.tracklist = [];
this.tracklist.push(...tracklist);
writeQueue(this.from, this.tracklist);
}
},
playFromFolder(fpath: string, tracks: Track[]) {
@ -235,7 +200,7 @@ export default defineStore("Queue", {
},
addTrackToQueue(track: Track) {
this.tracklist.push(track);
writeQueue(this.from, this.tracklist);
// writeQueue(this.from, this.tracklist);
this.updateNext(this.current);
},
playTrackNext(track: Track) {
@ -264,16 +229,12 @@ export default defineStore("Queue", {
`Added ${track.title} to queue`,
NotifType.Success
);
writeQueue(this.from, this.tracklist);
},
clearQueue() {
this.tracklist = [] as Track[];
this.currentid = "";
this.current, this.next, (this.prev = 0);
this.from = <From>{};
writeCurrent(0);
writeQueue(this.from, [] as Track[]);
},
shuffleQueue() {
const Toast = useNotifStore();
@ -291,12 +252,38 @@ export default defineStore("Queue", {
this.currentid = shuffled[0].trackid;
this.next = 1;
this.prev = this.tracklist.length - 1;
writeQueue(this.from, shuffled);
writeCurrent(0);
},
removeFromQueue(index: number = 0) {
this.tracklist.splice(index, 1);
},
},
getters: {
getNextTrack() {
if (this.current == this.tracklist.length - 1) {
return this.tracklist[0];
} else {
return this.tracklist[this.current + 1];
}
},
getPrevTrack() {
if (this.current === 0) {
return this.tracklist[this.tracklist.length - 1];
} else {
return this.tracklist[this.current - 1];
}
},
fullTime() {
return audio.duration;
},
currentTime() {
return audio.currentTime;
},
getCurrentTrack() {
return this.tracklist[this.current];
},
getIsplaying() {
return audio.paused ? false : true;
},
},
persist: true,
});

View File

@ -106,7 +106,8 @@ onBeforeRouteUpdate((to, from) => {
height: 100%;
width: 100%;
object-fit: cover;
object-position: bottom;
object-position: bottom right;
transition: all .25s ease;
}
}

View File

@ -14,8 +14,8 @@
<script setup lang="ts">
import PlaylistCard from "@/components/playlists/PlaylistCard.vue";
import usePStore from "@/stores/pages/playlists";
import NewPlaylistCard from "@/components/playlists/NewPlaylistCard.vue";
import usePStore from "@/stores/pages/playlists";
const pStore = usePStore();
</script>

7
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@ -1,11 +1,40 @@
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
// "strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"paths": {
"baseUrl": ["./"],
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"]
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/**/*.svg"
],
"references": [{ "path": "./tsconfig.node.json" }]
}
// {
// "compilerOptions": {
// "strict": false,
// "jsx": "preserve",
// "paths": {
// "baseUrl": ["./"],
// "@/*": ["./src/*"]
// }
// },
// "include": ["src/**/*"]
// }

9
tsconfig.node.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -1,5 +1,5 @@
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import svgLoader from "vite-svg-loader";
const path = require("path");