add eslint

This commit is contained in:
Thomas Amland 2020-08-10 11:11:40 +02:00
parent 9f842bcffe
commit 8e0cc715ab
50 changed files with 2493 additions and 1059 deletions

View File

@ -3,3 +3,4 @@ indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100

38
.eslintrc.js Normal file
View File

@ -0,0 +1,38 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/recommended',
'eslint:recommended',
'@vue/standard',
'@vue/typescript',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'vue/script-indent': ['error', 2, { baseIndent: 1 }],
'vue/no-unused-components': 'off',
'vue/max-attributes-per-line': 'off',
'vue/html-closing-bracket-newline': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-useless-constructor': 'off', // Crashes eslint
'no-empty-pattern': 'off',
'comma-dangle': 'off',
'space-before-function-paren': ['error', 'never'],
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
overrides: [
{
files: ['*.vue'],
rules: {
indent: 'off'
}
}
]
}

View File

@ -4,7 +4,8 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.19.0",
@ -19,11 +20,22 @@
},
"devDependencies": {
"@types/howler": "^2.2.1",
"@vue/cli-plugin-babel": "^4.4.6",
"@vue/cli-plugin-typescript": "^4.4.6",
"@vue/cli-service": "^4.4.6",
"@typescript-eslint/eslint-plugin": "^3.9.0",
"@typescript-eslint/parser": "^3.9.0",
"@vue/cli-plugin-babel": "^4.5.3",
"@vue/cli-plugin-eslint": "~4.5.3",
"@vue/cli-plugin-typescript": "^4.5.3",
"@vue/cli-service": "^4.5.3",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/eslint-config-typescript": "^5.0.2",
"eslint": "^7.6.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2",
"sass": "^1.26.10",
"sass-loader": "^9.0.2",
"sass-loader": "^9.0.3",
"typescript": "^3.9.7",
"vue-template-compiler": "^2.6.10"
},

View File

@ -1,15 +1,15 @@
<template>
<div>
<div class="min-vh-100 d-flex align-items-stretch justify-spcace-between">
<Sidebar/>
<Sidebar />
<div class="main flex-fill">
<div class="container-fluid pt-3 pb-3">
<TopNav/>
<router-view></router-view>
<TopNav />
<router-view />
</div>
</div>
</div>
<ErrorBar/>
<ErrorBar />
<footer class="footer elevated">
<Player />
</footer>
@ -28,10 +28,10 @@
}
</style>
<script lang="ts">
import ErrorBar from "./ErrorBar.vue";
import TopNav from "./TopNav.vue";
import Sidebar from "./Sidebar.vue";
import Player from "@/player/Player.vue";
import ErrorBar from './ErrorBar.vue'
import TopNav from './TopNav.vue'
import Sidebar from './Sidebar.vue'
import Player from '@/player/Player.vue'
export default {
components: {
@ -40,5 +40,5 @@
Sidebar,
Player,
},
};
}
</script>

View File

@ -12,8 +12,8 @@
</b-alert>
</template>
<script lang="ts">
import Vue from "vue";
import { mapState, mapMutations } from 'vuex';
import Vue from 'vue'
import { mapState, mapMutations } from 'vuex'
export default Vue.extend({
computed: {
@ -24,5 +24,5 @@
'clearError'
]),
},
});
</script>
})
</script>

View File

@ -20,4 +20,4 @@
svg {
fill: var(--primary);
}
</style>
</style>

View File

@ -2,21 +2,21 @@
<div class="text-truncate">
<nav class="nav flex-column">
<router-link class="nav-link logo" :to="{name: 'home'}">
<Logo/>
<Logo />
</router-link>
<router-link class="nav-link" :to="{name: 'home'}">
<Icon icon="card-text" class="mr-2"/> Home
<Icon icon="card-text" class="mr-2" /> Home
</router-link>
<router-link class="nav-link" :to="{name: 'queue'}">
<Icon icon="music-note-list" class="mr-2"/> Playing
<Icon icon="music-note-list" class="mr-2" /> Playing
</router-link>
<router-link class="nav-link" :to="{name: 'starred'}">
<Icon icon="star-fill" class="mr-2"/> Starred
<Icon icon="star-fill" class="mr-2" /> Starred
</router-link>
<a class="nav-link disabled">
<small class="text-uppercase text-muted font-weight-bold">
Library
@ -24,18 +24,18 @@
</a>
<router-link class="nav-link" :to="{name: 'albums'}">
<Icon icon="collection-fill" class="mr-2"/> Albums
<Icon icon="collection-fill" class="mr-2" /> Albums
</router-link>
<router-link class="nav-link" :to="{name: 'artists'}">
<Icon icon="collection-fill" class="mr-2"/> Artists
<Icon icon="collection-fill" class="mr-2" /> Artists
</router-link>
<router-link class="nav-item nav-link" :to="{name: 'genres'}">
<Icon icon="collection-fill" class="mr-2"/> Genres
<Icon icon="collection-fill" class="mr-2" /> Genres
</router-link>
<PlaylistNav/>
<PlaylistNav />
</nav>
</div>
</template>
@ -51,21 +51,20 @@
}
</style>
<script lang="ts">
import Vue from "vue";
import Logo from "./Logo.vue";
import PlaylistNav from "@/playlist/PlaylistNav.vue";
import { mapState } from 'vuex';
import Vue from 'vue'
import Logo from './Logo.vue'
import PlaylistNav from '@/playlist/PlaylistNav.vue'
export default Vue.extend({
components: {
Logo,
PlaylistNav,
},
methods: {
logout() {
this.$auth.logout();
this.$router.go(0);
export default Vue.extend({
components: {
Logo,
PlaylistNav,
},
}
});
</script>
methods: {
logout() {
this.$auth.logout()
this.$router.go(0)
},
}
})
</script>

View File

@ -1,20 +1,20 @@
<template>
<div class="sidebar elevated">
<div class="d-none d-md-block">
<Nav/>
<Nav />
</div>
<b-sidebar
:visible="showMenu"
@hidden="toggleMenu"
class="d-md-none"
sidebar-class="elevated"
bg-variant=""
shadow="lg"
no-header
backdrop
backdrop-variant=""
>
<Nav/>
:visible="showMenu"
class="d-md-none"
sidebar-class="elevated"
bg-variant=""
shadow="lg"
no-header
backdrop
backdrop-variant=""
@hidden="toggleMenu"
>
<Nav />
</b-sidebar>
</div>
</template>
@ -24,9 +24,9 @@
}
</style>
<script lang="ts">
import Vue from "vue";
import Nav from "./Nav.vue";
import { mapState, mapMutations } from 'vuex';
import Vue from 'vue'
import Nav from './Nav.vue'
import { mapState, mapMutations } from 'vuex'
export default Vue.extend({
components: {
@ -38,5 +38,5 @@
methods: {
...mapMutations(['toggleMenu']),
},
});
</script>
})
</script>

View File

@ -1,18 +1,18 @@
<template>
<div class="d-flex align-items-center mb-2">
<button class="navbar-toggler text-white d-md-none" @click="toggleMenu">
<Icon icon="list"/>
<Icon icon="list" />
</button>
<div class="ml-auto"></div>
<div class="ml-auto" />
<SearchForm/>
<SearchForm />
<template v-if="username">
<b-dropdown variant="link" right no-caret>
<template #button-content>
<b-avatar variant="secondary">
<Icon icon="person-fill"/>
<Icon icon="person-fill" />
</b-avatar>
</template>
<b-dropdown-text>
@ -21,7 +21,7 @@
<b-dropdown-text>
{{ username }}
</b-dropdown-text>
<b-dropdown-divider/>
<b-dropdown-divider />
<b-dropdown-item-button @click="logout">
Log out
</b-dropdown-item-button>
@ -30,9 +30,9 @@
</div>
</template>
<script lang="ts">
import Vue from "vue";
import { mapMutations, mapState } from 'vuex';
import SearchForm from '@/search/SearchForm.vue';
import Vue from 'vue'
import { mapMutations, mapState } from 'vuex'
import SearchForm from '@/search/SearchForm.vue'
export default Vue.extend({
components: {
@ -40,8 +40,8 @@
},
computed: {
...mapState([
"server",
"username",
'server',
'username',
]),
},
methods: {
@ -49,9 +49,9 @@
'toggleMenu',
]),
logout() {
this.$auth.logout();
this.$router.go(0);
this.$auth.logout()
this.$router.go(0)
}
}
});
</script>
})
</script>

View File

@ -3,86 +3,85 @@
<b-modal size="sm" hide-header hide-footer no-close-on-esc :visible="showModal">
<form @submit.prevent="login">
<div style="font-size: 4rem; color: #fff;" class="text-center">
<Icon icon="person-circle"/>
<Icon icon="person-circle" />
</div>
<b-form-group label="Server">
<b-form-input name="server" type="text" v-model="server" :state="valid"/>
<b-form-input v-model="server" name="server" type="text" :state="valid" />
</b-form-group>
<b-form-group label="Username">
<b-form-input name="username" type="text" v-model="username" :state="valid"/>
<b-form-input v-model="username" name="username" type="text" :state="valid" />
</b-form-group>
<b-form-group label="Password">
<b-form-input name="password" type="password" v-model="password" :state="valid"/>
<b-form-input v-model="password" name="password" type="password" :state="valid" />
</b-form-group>
<b-alert :show="error != null" variant="danger">
<template v-if="error != null">
Could not log in. ({{ error.message }})
</template>
</b-alert>
<button class="btn btn-primary btn-block" @click="login" :disabled="busy">
<b-spinner v-show="busy" small type="grow"/> Log in
<button class="btn btn-primary btn-block" :disabled="busy" @click="login">
<b-spinner v-show="busy" small type="grow" /> Log in
</button>
</form>
</b-modal>
</div>
</template>>
<script lang="ts">
import Vue from "vue";
import { mapMutations, mapState } from "vuex";
import Vue from 'vue'
export default Vue.extend({
props: {
returnTo: { type: String, required: true },
},
data() {
return {
server: "",
username: "",
password: "",
rememberLogin: true,
busy: false,
error: null,
showModal: false,
};
},
async created() {
this.server = await this.$auth.server;
this.username = await this.$auth.username;
const success = await this.$auth.autoLogin();
if (success) {
this.$store.commit("setLoginSuccess", {
username: this.username,
server: this.server,
});
this.$router.replace(this.returnTo);
} else {
this.showModal = true;
}
},
computed: {
valid(): false | null {
return this.error == null ? null : false
}
},
methods: {
login() {
this.error = null;
this.busy = true;
this.$auth.loginWithPassword(this.server, this.username, this.password, this.rememberLogin)
.then(() => {
this.$store.commit("setLoginSuccess", {
username: this.username,
server: this.server,
});
this.$router.replace(this.returnTo);
export default Vue.extend({
props: {
returnTo: { type: String, required: true },
},
data() {
return {
server: '',
username: '',
password: '',
rememberLogin: true,
busy: false,
error: null,
showModal: false,
}
},
computed: {
valid(): false | null {
return this.error == null ? null : false
}
},
async created() {
this.server = await this.$auth.server
this.username = await this.$auth.username
const success = await this.$auth.autoLogin()
if (success) {
this.$store.commit('setLoginSuccess', {
username: this.username,
server: this.server,
})
.catch(err => {
this.error = err;
})
.finally(() => {
this.busy = false;
});
this.$router.replace(this.returnTo)
} else {
this.showModal = true
}
},
methods: {
login() {
this.error = null
this.busy = true
this.$auth.loginWithPassword(this.server, this.username, this.password, this.rememberLogin)
.then(() => {
this.$store.commit('setLoginSuccess', {
username: this.username,
server: this.server,
})
this.$router.replace(this.returnTo)
})
.catch(err => {
this.error = err
})
.finally(() => {
this.busy = false
})
}
}
}
});
})
</script>

View File

@ -1,69 +1,74 @@
import axios from 'axios';
import { randomString, md5 } from '@/shared/utils';
import axios from 'axios'
import { randomString, md5 } from '@/shared/utils'
export class AuthService {
public server: string = "";
public username: string = "";
public salt: string = "";
public hash: string = "";
private authenticated: boolean = false;
public server = '';
public username = '';
public salt = '';
public hash = '';
private authenticated = false;
constructor() {
this.server = localStorage.getItem("server") || "/api";
this.username = localStorage.getItem("username") || "guest1";
this.salt = localStorage.getItem("salt") || "";
this.hash = localStorage.getItem("hash") || "";
this.server = localStorage.getItem('server') || '/api'
this.username = localStorage.getItem('username') || 'guest1'
this.salt = localStorage.getItem('salt') || ''
this.hash = localStorage.getItem('hash') || ''
}
private saveSession() {
localStorage.setItem("server", this.server);
localStorage.setItem("username", this.username);
localStorage.setItem("salt", this.salt);
localStorage.setItem("hash", this.hash);
localStorage.setItem('server', this.server)
localStorage.setItem('username', this.username)
localStorage.setItem('salt', this.salt)
localStorage.setItem('hash', this.hash)
}
async autoLogin(): Promise<boolean> {
if (!this.server || !this.username) {
return false;
return false
}
return this.loginWithHash(this.server, this.username, this.salt, this.hash, false)
.then(() => true)
.catch(() => false);
.catch(() => false)
}
async loginWithPassword(server: string, username: string, password: string, remember: boolean) {
const salt = randomString();
const hash = md5(password + salt);
return this.loginWithHash(server, username, salt, hash, remember);
const salt = randomString()
const hash = md5(password + salt)
return this.loginWithHash(server, username, salt, hash, remember)
}
private async loginWithHash(server: string, username: string, salt: string, hash: string, remember: boolean) {
return axios.get(`${server}/rest/ping.view?u=${username}&s=${salt}&t=${hash}&v=1.15.0&c=app&f=json`)
private async loginWithHash(
server: string,
username: string,
salt: string,
hash: string,
remember: boolean
) {
const url = `${server}/rest/ping.view?u=${username}&s=${salt}&t=${hash}&v=1.15.0&c=app&f=json`
return axios.get(url)
.then((response) => {
const subsonicResponse = response.data["subsonic-response"];
if (!subsonicResponse || subsonicResponse.status !== "ok") {
const err = new Error(subsonicResponse.status);
return Promise.reject(err);
const subsonicResponse = response.data['subsonic-response']
if (!subsonicResponse || subsonicResponse.status !== 'ok') {
const err = new Error(subsonicResponse.status)
return Promise.reject(err)
}
this.authenticated = true;
this.server = server;
this.username = username;
this.salt = salt;
this.hash = hash;
this.authenticated = true
this.server = server
this.username = username
this.salt = salt
this.hash = hash
if (remember) {
this.saveSession();
this.saveSession()
}
})
}
logout() {
localStorage.clear();
sessionStorage.clear();
localStorage.clear()
sessionStorage.clear()
}
isAuthenticated() {
return this.authenticated;
return this.authenticated
}
}
}

17
src/global.d.ts vendored
View File

@ -3,7 +3,7 @@ declare module '*.vue' {
export default Vue
}
declare module "md5-es";
declare module 'md5-es';
interface Navigator {
readonly mediaSession?: MediaSession;
@ -15,7 +15,16 @@ interface Window {
type MediaSessionPlaybackState = 'none' | 'paused' | 'playing';
type MediaSessionAction = 'play' | 'pause' | 'seekbackward' | 'seekforward' | 'seekto' | 'previoustrack' | 'nexttrack' | 'skipad' | 'stop';
type MediaSessionAction =
'play' |
'pause' |
'seekbackward' |
'seekforward' |
'seekto' |
'previoustrack' |
'nexttrack' |
'skipad' |
'stop';
interface MediaSessionActionDetails {
action: MediaSessionAction;
@ -33,7 +42,9 @@ interface MediaPositionState {
interface MediaSession {
playbackState: MediaSessionPlaybackState;
metadata: MediaMetadata | null;
setActionHandler(action: MediaSessionAction, listener: ((details: MediaSessionActionDetails) => void)): void;
setActionHandler(
action: MediaSessionAction,
listener: ((details: MediaSessionActionDetails) => void)): void;
setPositionState?(arg: MediaPositionState): void;
}

View File

@ -1,52 +1,52 @@
<template>
<div>
<Spinner v-if="loading"/>
<Spinner v-if="loading" />
<template v-else>
<div v-for="section in sections" :key="section.key" class="mb-4">
<h1>{{ section.name }}</h1>
<AlbumList :items="$data[section.key]"/>
<AlbumList :items="$data[section.key]" />
</div>
</template>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import AlbumList from "@/library/album/AlbumList.vue";
import Vue from 'vue'
import AlbumList from '@/library/album/AlbumList.vue'
export default Vue.extend({
components: {
AlbumList,
},
data() {
return {
sections: [
{ name: "Recently played", key: "recent" },
{ name: "Recently added", key: "newest" },
{ name: "Most played", key: "frequent" },
{ name: "Random", key: "random" },
],
loading: true as boolean,
recent: [],
newest: [],
frequent: [],
random: [],
export default Vue.extend({
components: {
AlbumList,
},
data() {
return {
sections: [
{ name: 'Recently played', key: 'recent' },
{ name: 'Recently added', key: 'newest' },
{ name: 'Most played', key: 'frequent' },
{ name: 'Random', key: 'random' },
],
loading: true as boolean,
recent: [],
newest: [],
frequent: [],
random: [],
}
},
created() {
const size = 10
this.$api.getAlbums('recent', size).then(result => {
this.recent = result
this.loading = false
})
this.$api.getAlbums('newest', size).then(result => {
this.newest = result
})
this.$api.getAlbums('frequent', size).then(result => {
this.frequent = result
})
this.$api.getAlbums('random', 50).then(result => {
this.random = result
})
}
},
created() {
const size = 10;
this.$api.getAlbums("recent", size).then(result => {
this.recent = result;
this.loading = false;
});
this.$api.getAlbums("newest", size).then(result => {
this.newest = result
});
this.$api.getAlbums("frequent", size).then(result => {
this.frequent = result
});
this.$api.getAlbums("random", 50).then(result => {
this.random = result
});
}
});
})
</script>

View File

@ -1,7 +1,7 @@
<template>
<b-dropdown variant="link" boundary="window" no-caret toggle-class="p-0">
<template #button-content>
<Icon icon="three-dots-vertical"/>
<Icon icon="three-dots-vertical" />
</template>
<b-dropdown-item-button @click="playNext()">
Play next
@ -12,11 +12,11 @@
<b-dropdown-item-button @click="toggleStarred()">
{{ starred ? 'Unstar' : 'Star' }}
</b-dropdown-item-button>
<slot :item="track"></slot>
<slot :item="track" />
</b-dropdown>
</template>
<script lang="ts">
import Vue from "vue";
import Vue from 'vue'
export default Vue.extend({
props: {
@ -30,18 +30,18 @@
methods: {
toggleStarred() {
if (this.starred) {
this.$api.unstar('track', this.track.id);
this.$api.unstar('track', this.track.id)
} else {
this.$api.star('track', this.track.id);
this.$api.star('track', this.track.id)
}
this.starred = !this.starred;
this.starred = !this.starred
},
playNext() {
return this.$store.dispatch("player/playNext", this.track);
return this.$store.dispatch('player/playNext', this.track)
},
addToQueue() {
return this.$store.dispatch("player/addToQueue", this.track);
return this.$store.dispatch('player/addToQueue', this.track)
},
}
});
})
</script>

View File

@ -1,65 +1,73 @@
<template>
<div>
<b-table-simple borderless hover>
<thead>
<tr>
<th class="pl-0 pr-0 text-center text-muted"></th>
<th class="text-left">Title</th>
<th class="text-left d-none d-lg-table-cell">Artist</th>
<th class="text-left d-none d-md-table-cell" v-if="showAlbum">Album</th>
<th class="text-right d-none d-md-table-cell">Duration</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in tracks" :key="index"
draggable="true" @dragstart="dragstart(item.id, $event)"
:class="{'text-primary': item.id === playingTrackId}">
<td class="pl-0 pr-0 text-center text-muted"
style="min-width: 20px; max-width: 20px;"
@click="play(index)">
<template v-if="item.id === playingTrackId">
<Icon :icon="isPlaying ? 'pause-fill' : 'play-fill'" class="text-primary"/>
</template>
<template v-else>
<span class="track-number">{{ item.track || 1 }}</span>
<Icon class="track-number-hover" icon="play-fill"/>
</template>
</td>
<td @click="play(index)">
{{ item.title }}
<div class="d-lg-none text-muted">
<small>{{ item.artist }}</small>
</div>
</td>
<td class="d-none d-lg-table-cell">
<template v-if="item.artistId">
<router-link :to="{name: 'artist', params: {id: item.artistId}}">
{{ item.artist }}
</router-link>
</template>
<template v-else>
{{ item.artist }}
</template>
</td>
<td class="d-none d-md-table-cell" v-if="showAlbum">
<router-link :to="{name: 'album', params: {id: item.albumId}}">
{{ item.album }}
</router-link>
</td>
<td class="text-right d-none d-md-table-cell">
{{ item.duration | duration }}
</td>
<td class="text-right">
<TrackContextMenu :track="item">
<template v-slot="{ item }">
<slot name="context-menu" :index="index" :item="item"></slot>
<thead>
<tr>
<th class="pl-0 pr-0 text-center text-muted" />
<th class="text-left">
Title
</th>
<th class="text-left d-none d-lg-table-cell">
Artist
</th>
<th v-if="showAlbum" class="text-left d-none d-md-table-cell">
Album
</th>
<th class="text-right d-none d-md-table-cell">
Duration
</th>
<th class="text-right">
Actions
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in tracks" :key="index"
draggable="true" :class="{'text-primary': item.id === playingTrackId}"
@dragstart="dragstart(item.id, $event)">
<td class="pl-0 pr-0 text-center text-muted"
style="min-width: 20px; max-width: 20px;"
@click="play(index)">
<template v-if="item.id === playingTrackId">
<Icon :icon="isPlaying ? 'pause-fill' : 'play-fill'" class="text-primary" />
</template>
</TrackContextMenu>
</td>
</tr>
</tbody>
</b-table-simple>
<template v-else>
<span class="track-number">{{ item.track || 1 }}</span>
<Icon class="track-number-hover" icon="play-fill" />
</template>
</td>
<td @click="play(index)">
{{ item.title }}
<div class="d-lg-none text-muted">
<small>{{ item.artist }}</small>
</div>
</td>
<td class="d-none d-lg-table-cell">
<template v-if="item.artistId">
<router-link :to="{name: 'artist', params: {id: item.artistId}}">
{{ item.artist }}
</router-link>
</template>
<template v-else>
{{ item.artist }}
</template>
</td>
<td v-if="showAlbum" class="d-none d-md-table-cell">
<router-link :to="{name: 'album', params: {id: item.albumId}}">
{{ item.album }}
</router-link>
</td>
<td class="text-right d-none d-md-table-cell">
{{ item.duration | duration }}
</td>
<td class="text-right">
<TrackContextMenu :track="item">
<slot name="context-menu" :index="index" :item="item" />
</TrackContextMenu>
</td>
</tr>
</tbody>
</b-table-simple>
</div>
</template>
<style lang="scss" scoped>
@ -76,43 +84,43 @@
}
</style>
<script lang="ts">
import Vue from "vue";
import { mapActions, mapMutations, mapGetters, mapState } from 'vuex';
import TrackContextMenu from "@/library/TrackContextMenu.vue"
import Vue from 'vue'
import { mapActions, mapGetters, mapState } from 'vuex'
import TrackContextMenu from '@/library/TrackContextMenu.vue'
export default Vue.extend({
components: {
TrackContextMenu,
},
props: {
tracks: { type: Array, required: true },
showAlbum: { type: Boolean, default: false },
},
computed: {
...mapState("player", {
isPlaying: "isPlaying",
}),
...mapGetters({
playingTrackId: "player/trackId",
}),
},
methods: {
...mapActions({
playPause: "player/playPause",
}),
play(index: number) {
if ((this.tracks as any)[index].id === this.playingTrackId) {
return this.$store.dispatch("player/playPause")
}
return this.$store.dispatch('player/playQueue', {
index,
queue: this.tracks,
})
export default Vue.extend({
components: {
TrackContextMenu,
},
dragstart(id: string, event: any) {
console.log("dragstart: " + id)
event.dataTransfer.setData("id", id);
props: {
tracks: { type: Array, required: true },
showAlbum: { type: Boolean, default: false },
},
}
});
computed: {
...mapState('player', {
isPlaying: 'isPlaying',
}),
...mapGetters({
playingTrackId: 'player/trackId',
}),
},
methods: {
...mapActions({
playPause: 'player/playPause',
}),
play(index: number) {
if ((this.tracks as any)[index].id === this.playingTrackId) {
return this.$store.dispatch('player/playPause')
}
return this.$store.dispatch('player/playQueue', {
index,
queue: this.tracks,
})
},
dragstart(id: string, event: any) {
console.log('dragstart: ' + id)
event.dataTransfer.setData('id', id)
},
}
})
</script>

View File

@ -1,10 +1,11 @@
<template>
<div v-if="album">
<div class="d-flex mb-3">
<b-img height="300" width="300" fluid :src="album.image"/>
<b-img height="300" width="300" fluid :src="album.image" />
<div class="ml-3 ml-md-4">
<h1>{{ album.name }}</h1>
<p>by
<p>
by
<router-link :to="{name: 'artist', params: { id: album.artistId }}">
{{ album.artist }}
</router-link>
@ -15,7 +16,7 @@
</div>
<div class="row">
<div class="col">
<TrackList :tracks="album.song"/>
<TrackList :tracks="album.song" />
</div>
</div>
</div>
@ -26,23 +27,23 @@
}
</style>
<script lang="ts">
import Vue from "vue";
import TrackList from "@/library/TrackList.vue"
import Vue from 'vue'
import TrackList from '@/library/TrackList.vue'
export default Vue.extend({
components: {
TrackList,
},
props: {
id: String
},
data() {
return {
album: null,
export default Vue.extend({
components: {
TrackList,
},
props: {
id: { type: String, required: true }
},
data() {
return {
album: null,
}
},
async mounted() {
this.album = await this.$api.getAlbumDetails(this.id)
}
},
async mounted() {
this.album = await this.$api.getAlbumDetails(this.id);
}
});
})
</script>

View File

@ -1,50 +1,47 @@
<template>
<div>
<Spinner :data="albums" v-slot="{albums: data}">
<AlbumList :items="albums"/>
<Spinner v-slot="{ data }" :data="albums">
<AlbumList :items="data" />
</Spinner>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import AlbumList from './AlbumList.vue';
import { AlbumSort } from '@/shared/api';
import Vue from 'vue'
import AlbumList from './AlbumList.vue'
import { AlbumSort } from '@/shared/api'
export default Vue.extend({
components: {
AlbumList,
},
props: {
msg: String
},
data() {
return {
sort: "newest",
albums: null,
};
},
computed: {
sortOptions() {
return [
{ text: "A-Z", value: "alphabeticalByName" },
{ text: "Date", value: "newest" },
{ text: "frequent", value: "frequent" },
export default Vue.extend({
components: {
AlbumList,
},
data() {
return {
sort: 'newest',
albums: null,
}
},
computed: {
sortOptions() {
return [
{ text: 'A-Z', value: 'alphabeticalByName' },
{ text: 'Date', value: 'newest' },
{ text: 'frequent', value: 'frequent' },
// { text: "random", value: "random" },
// { text: "recent", value: "recent" },
// { text: "starred", value: "starred" },
]
}
},
watch: {
sort: {
immediate: true,
handler(value: AlbumSort) {
this.albums = null;
this.$api.getAlbums(value).then(albums => {
this.albums = albums;
});
]
}
}
},
});
},
watch: {
sort: {
immediate: true,
handler(value: AlbumSort) {
this.albums = null
this.$api.getAlbums(value).then(albums => {
this.albums = albums
})
}
}
},
})
</script>

View File

@ -1,9 +1,9 @@
<template functional>
<Tiles square>
<Tile v-for="item in props.items" :key="item.id"
:image="item.image"
:to="{name: 'album', params: { id: item.id } }"
:title="item.name">
:image="item.image"
:to="{name: 'album', params: { id: item.id } }"
:title="item.name">
<template v-slot:text>
<router-link :to="{name: 'artist', params: { id: item.artistId } }" class="text-muted">
{{ item.artist }}

View File

@ -7,46 +7,53 @@
<ExternalLink v-if="item.lastFmUrl" :href="item.lastFmUrl" class="btn btn-secondary mr-2">
Last.fm
</ExternalLink>
<ExternalLink v-if="item.musicBrainzUrl" :href="item.musicBrainzUrl" class="btn btn-secondary">
<ExternalLink
v-if="item.musicBrainzUrl"
:href="item.musicBrainzUrl"
class="btn btn-secondary">
MusicBrainz
</ExternalLink>
</div>
</div>
<h3 class="pt-5">Albums</h3>
<AlbumList :items="item.albums"/>
<h3 class="pt-5">
Albums
</h3>
<AlbumList :items="item.albums" />
<template v-if="item.similarArtist.length > 0">
<h3 class="pt-5">Similar artists</h3>
<ArtistList :items="item.similarArtist"/>
<h3 class="pt-5">
Similar artists
</h3>
<ArtistList :items="item.similarArtist" />
</template>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import AlbumList from "@/library/album/AlbumList.vue";
import ArtistList from "@/library/artist/ArtistList.vue";
import Vue from 'vue'
import AlbumList from '@/library/album/AlbumList.vue'
import ArtistList from '@/library/artist/ArtistList.vue'
export default Vue.extend({
components: {
AlbumList,
ArtistList,
},
props: {
id: String
},
data() {
return {
item: null as any,
}
},
watch: {
id: {
immediate: true,
async handler(value: string) {
this.item = null,
this.item = await this.$api.getArtistDetails(this.id);
export default Vue.extend({
components: {
AlbumList,
ArtistList,
},
props: {
id: { type: String, required: true }
},
data() {
return {
item: null as any,
}
},
watch: {
id: {
immediate: true,
async handler(value: string) {
this.item = null
this.item = await this.$api.getArtistDetails(value)
}
}
}
}
});
})
</script>

View File

@ -1,9 +1,9 @@
<template>
<ArtistList :items="items"/>
<ArtistList :items="items" />
</template>
<script lang="ts">
import Vue from "vue";
import ArtistList from './ArtistList.vue';
import Vue from 'vue'
import ArtistList from './ArtistList.vue'
export default Vue.extend({
components: {
@ -12,12 +12,12 @@
data() {
return {
items: []
};
}
},
created() {
this.$api.getArtists().then(items => {
this.items = items;
});
this.items = items
})
}
});
})
</script>

View File

@ -1,8 +1,8 @@
<template functional>
<Tiles>
<Tile v-for="item in props.items" :key="item.id"
:to="{name: 'artist', params: { id: item.id } }"
:title="item.name">
:to="{name: 'artist', params: { id: item.id } }"
:title="item.name">
<template v-slot:text>
<strong>{{ item.albumCount }}</strong> albums
</template>

View File

@ -1,27 +1,27 @@
<template>
<div v-if="item">
<h1>{{ item.name }}</h1>
<TrackList :tracks="item.tracks" show-album/>
<TrackList :tracks="item.tracks" show-album />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import TrackList from "@/library/TrackList.vue"
import Vue from 'vue'
import TrackList from '@/library/TrackList.vue'
export default Vue.extend({
components: {
TrackList,
},
props: {
id: String
},
data() {
return {
item: null as any,
export default Vue.extend({
components: {
TrackList,
},
props: {
id: { type: String, required: true }
},
data() {
return {
item: null as any,
}
},
async created() {
this.item = await this.$api.getGenreDetails(this.id)
}
},
async created() {
this.item = await this.$api.getGenreDetails(this.id);
}
});
})
</script>

View File

@ -1,9 +1,9 @@
<template>
<Tiles>
<Tile v-for="item in items" :key="item.id"
:image="item.image"
:to="{name: 'genre', params: { id: item.id } }"
:title="item.name">
:image="item.image"
:to="{name: 'genre', params: { id: item.id } }"
:title="item.name">
<template v-slot:text>
<strong>{{ item.albumCount }}</strong> albums ·
<strong>{{ item.songCount }}</strong> songs
@ -12,19 +12,19 @@
</Tiles>
</template>
<script lang="ts">
import Vue from "vue";
import Vue from 'vue'
export default Vue.extend({
components: {},
data() {
return {
items: [],
};
},
created() {
this.$api.getGenres().then((items) => {
this.items = items;
});
},
});
export default Vue.extend({
components: {},
data() {
return {
items: [],
}
},
created() {
this.$api.getGenres().then((items) => {
this.items = items
})
},
})
</script>

View File

@ -1,9 +1,9 @@
<template>
<TrackList v-if="items" :tracks="items"/>
<TrackList v-if="items" :tracks="items" />
</template>
<script lang="ts">
import Vue from "vue";
import TrackList from "@/library/TrackList.vue"
import Vue from 'vue'
import TrackList from '@/library/TrackList.vue'
export default Vue.extend({
components: {
@ -16,8 +16,8 @@
},
created() {
this.$api.getStarred().then(result => {
this.items = result;
this.items = result
})
}
});
})
</script>

View File

@ -1,15 +1,15 @@
import Vue from 'vue'
import Router from "vue-router"
import Vuex from "vuex"
import Router from 'vue-router'
import Vuex from 'vuex'
import { BootstrapVue } from 'bootstrap-vue'
import '@/style/main.scss'
import '@/shared/components'
import '@/shared/filters'
import App from '@/app/App.vue'
import {setupRouter} from '@/shared/router'
import {setupStore} from '@/shared/store'
import { API } from '@/shared/api';
import { AuthService } from '@/auth/service';
import { setupRouter } from '@/shared/router'
import { setupStore } from '@/shared/store'
import { API } from '@/shared/api'
import { AuthService } from '@/auth/service'
import { setupAudio } from './player/store'
declare module 'vue/types/vue' {
@ -21,20 +21,20 @@ declare module 'vue/types/vue' {
Vue.config.productionTip = false
Vue.use(Router)
Vue.use(Vuex);
Vue.use(Vuex)
Vue.use(BootstrapVue)
const authService = new AuthService();
const api = new API(authService);
const router = setupRouter(authService);
const store = setupStore(authService, api);
setupAudio(store);
const authService = new AuthService()
const api = new API(authService)
const router = setupRouter(authService)
const store = setupStore(authService, api)
setupAudio(store)
Vue.prototype.$auth = authService;
Vue.prototype.$api = api;
Vue.prototype.$auth = authService
Vue.prototype.$api = api
Vue.config.errorHandler = (err, vm, info) => {
store.commit("setError", err)
Vue.config.errorHandler = (err) => {
store.commit('setError', err)
}
new Vue({

View File

@ -2,13 +2,13 @@
<div class="d-flex player">
<div v-if="track" class="d-none d-sm-block">
<router-link :to="{name: 'album', params: {id: track.albumId}}">
<b-img :src="track.image" block width="80px" height="80px"></b-img>
<b-img :src="track.image" block width="80px" height="80px" />
</router-link>
</div>
<div class="flex-fill">
<!-- Progress --->
<div class="progress2" @click="seek">
<b-progress :value="progress" :max="100" height="4px"></b-progress>
<b-progress :value="progress" :max="100" height="4px" />
</div>
<div class="row d-flex align-items-center p-2 m-0">
<!-- Track info --->
@ -26,13 +26,13 @@
<!-- Controls--->
<div class="col-auto p-0">
<b-button variant="link" class="m-2" @click="previous">
<Icon icon="skip-start-fill"/>
<Icon icon="skip-start-fill" />
</b-button>
<b-button variant="link" size="lg" class="m-2" @click="playPause">
<Icon :icon="isPlaying ? 'pause-fill' : 'play-fill'"/>
<Icon :icon="isPlaying ? 'pause-fill' : 'play-fill'" />
</b-button>
<b-button variant="link" class="m-2" @click="next">
<Icon icon="skip-end-fill"/>
<Icon icon="skip-end-fill" />
</b-button>
</div>
<div class="col p-0 text-truncate">
@ -50,39 +50,39 @@
}
</style>
<script lang="ts">
import Vue from "vue";
import { mapMutations, mapState, mapGetters, mapActions } from 'vuex';
import Vue from 'vue'
import { mapState, mapGetters, mapActions } from 'vuex'
export default Vue.extend({
data() {
return {
};
},
computed: {
...mapState("player", {
isPlaying: (state: any) => state.isPlaying,
currentTime: (state: any) => state.currentTime,
duration: (state: any) => state.duration,
}),
...mapGetters("player", [
"track",
"progress",
]),
},
methods: {
...mapActions("player", [
"playPause",
"next",
"previous",
]),
seek(event: any) {
if (event.target) {
const width = event.currentTarget.clientWidth;
const value = event.offsetX / width;
return this.$store.dispatch("player/seek", value);
export default Vue.extend({
data() {
return {
}
},
computed: {
...mapState('player', {
isPlaying: (state: any) => state.isPlaying,
currentTime: (state: any) => state.currentTime,
duration: (state: any) => state.duration,
}),
...mapGetters('player', [
'track',
'progress',
]),
},
methods: {
...mapActions('player', [
'playPause',
'next',
'previous',
]),
seek(event: any) {
if (event.target) {
const width = event.currentTarget.clientWidth
const value = event.offsetX / width
return this.$store.dispatch('player/seek', value)
}
}
}
}
});
})
</script>

View File

@ -10,23 +10,23 @@
</div>
</template>
<script lang="ts">
import Vue from "vue";
import { mapState, mapMutations, mapActions } from 'vuex';
import TrackList from "@/library/TrackList.vue";
import Vue from 'vue'
import { mapState, mapMutations } from 'vuex'
import TrackList from '@/library/TrackList.vue'
export default Vue.extend({
components: {
TrackList,
},
computed: {
...mapState("player", {
tracks: (state: any) => state.queue,
})
},
methods: {
...mapMutations("player", {
remove: "removeFromQueue",
}),
}
});
export default Vue.extend({
components: {
TrackList,
},
computed: {
...mapState('player', {
tracks: (state: any) => state.queue,
})
},
methods: {
...mapMutations('player', {
remove: 'removeFromQueue',
}),
}
})
</script>

View File

@ -1,9 +1,9 @@
<template>
<div>
<div class="col" d-flex fill-height grow>
<h1>{{ track.name }}</h1>
<div>{{ track.artist }}</div>
<v-card color=blue tile height="100%" width="100%"></v-card>
<h1>{{ track.name }}</h1>
<div>{{ track.artist }}</div>
<v-card color="blue" tile height="100%" width="100%" />
</div>
</div>
</template>
@ -18,11 +18,11 @@
</style>
<script lang="ts">
import Vue from "vue";
import Vue from 'vue'
export default Vue.extend({
props: {
album: Object
}
});
export default Vue.extend({
props: {
album: { type: Object, required: true }
}
})
</script>

View File

@ -1,12 +1,12 @@
import { Store, Module } from 'vuex'
const audio = new Audio();
const storedQueue = JSON.parse(localStorage.getItem("queue") || "[]");
const storedQueueIndex = parseInt(localStorage.getItem("queueIndex") || "-1");
const audio = new Audio()
const storedQueue = JSON.parse(localStorage.getItem('queue') || '[]')
const storedQueueIndex = parseInt(localStorage.getItem('queueIndex') || '-1')
if (storedQueueIndex > -1 && storedQueueIndex < storedQueue.length) {
audio.src = storedQueue[storedQueueIndex].url;
audio.src = storedQueue[storedQueueIndex].url
}
const mediaSession: MediaSession | undefined = navigator.mediaSession;
const mediaSession: MediaSession | undefined = navigator.mediaSession
interface State {
queue: any[];
@ -28,177 +28,176 @@ export const playerModule: Module<State, any> = {
mutations: {
setPlaying(state) {
state.isPlaying = true;
state.isPlaying = true
if (mediaSession) {
mediaSession.playbackState = "playing";
mediaSession.playbackState = 'playing'
}
},
setPaused(state) {
state.isPlaying = false;
state.isPlaying = false
if (mediaSession) {
mediaSession.playbackState = "paused";
mediaSession.playbackState = 'paused'
}
},
setPosition(state, time: number) {
audio.currentTime = time;
audio.currentTime = time
},
setQueue(state, queue) {
state.queue = queue;
state.queueIndex = -1;
localStorage.setItem("queue", JSON.stringify(queue));
state.queue = queue
state.queueIndex = -1
localStorage.setItem('queue', JSON.stringify(queue))
},
setQueueIndex(state, index) {
if (state.queue.length === 0) {
return;
return
}
index = Math.max(0, index);
index = index < state.queue.length ? index : 0;
state.queueIndex = index;
localStorage.setItem("queueIndex", index);
const track = state.queue[index];
audio.src = track.url;
index = Math.max(0, index)
index = index < state.queue.length ? index : 0
state.queueIndex = index
localStorage.setItem('queueIndex', index)
const track = state.queue[index]
audio.src = track.url
if (mediaSession) {
mediaSession.metadata = new MediaMetadata({
title: track.title,
artist: track.artist,
album: track.album,
artwork: track.image ? [{ src: track.image, sizes: "300x300" }] : undefined,
});
artwork: track.image ? [{ src: track.image, sizes: '300x300' }] : undefined,
})
}
},
addToQueue(state, track) {
state.queue.push(track);
state.queue.push(track)
},
removeFromQueue(state, index) {
state.queue.splice(index, 1);
state.queue.splice(index, 1)
if (index < state.queueIndex) {
state.queueIndex--;
state.queueIndex--
}
},
setNextInQueue(state, track) {
state.queue.splice(state.queueIndex + 1, 0, track);
state.queue.splice(state.queueIndex + 1, 0, track)
},
setProgress(state, value: any) {
state.currentTime = value;
state.currentTime = value
},
setDuration(state, value: any) {
state.duration = value;
state.duration = value
},
},
actions: {
async playQueue({ commit }, { queue, index }) {
commit("setQueue", [...queue]);
commit("setQueueIndex", index);
commit("setPlaying");
await audio.play();
commit('setQueue', [...queue])
commit('setQueueIndex', index)
commit('setPlaying')
await audio.play()
},
async play({ commit }) {
commit("setPlaying");
await audio.play();
commit('setPlaying')
await audio.play()
},
pause({ commit }) {
audio.pause();
commit("setPaused");
audio.pause()
commit('setPaused')
},
async next({ commit, state }) {
commit("setQueueIndex", state.queueIndex + 1);
commit("setPlaying");
await audio.play();
commit('setQueueIndex', state.queueIndex + 1)
commit('setPlaying')
await audio.play()
},
async previous({ commit, state }) {
commit("setQueueIndex", state.queueIndex - 1);
commit("setPlaying");
await audio.play();
commit('setQueueIndex', state.queueIndex - 1)
commit('setPlaying')
await audio.play()
},
playPause({ state, dispatch }) {
if (state.isPlaying) {
return dispatch("pause");
return dispatch('pause')
}
return dispatch("play");
return dispatch('play')
},
seek({ commit, state }, value) {
commit("setPosition", state.duration * value);
commit('setPosition', state.duration * value)
},
playNext({ commit }, track) {
commit("setNextInQueue", track);
commit('setNextInQueue', track)
},
addToQueue({ commit }, track) {
commit("addToQueue", track);
commit('addToQueue', track)
},
},
getters: {
track(state) {
if (state.queueIndex != -1) {
return state.queue[state.queueIndex];
if (state.queueIndex !== -1) {
return state.queue[state.queueIndex]
}
return null;
return null
},
trackId(state, getters): number {
return getters.track ? getters.track.id : -1;
return getters.track ? getters.track.id : -1
},
progress(state) {
if (state.currentTime > -1 && state.duration > 0) {
return (state.currentTime / state.duration) * 100;
return (state.currentTime / state.duration) * 100
}
return 0;
return 0
},
hasNext(state) {
return state.queueIndex < state.queue.length - 1;
return state.queueIndex < state.queue.length - 1
},
hasPrevious(state) {
return state.queueIndex > 0;
return state.queueIndex > 0
},
},
};
}
export function setupAudio(store: Store<any>) {
audio.ontimeupdate = (event) => {
store.commit("player/setProgress", audio.currentTime)
};
audio.ondurationchange = (event) => {
store.commit("player/setDuration", audio.duration);
audio.ontimeupdate = () => {
store.commit('player/setProgress', audio.currentTime)
}
audio.onended = (event) => {
store.dispatch("player/next");
audio.ondurationchange = () => {
store.commit('player/setDuration', audio.duration)
}
audio.onerror = (event) => {
store.commit("player/setPaused");
store.commit("setError", audio.error);
audio.onended = () => {
store.dispatch('player/next')
}
audio.onerror = () => {
store.commit('player/setPaused')
store.commit('setError', audio.error)
}
audio.onwaiting = () => {
console.log('audio is waiting for more data.')
}
audio.onwaiting = (event) => {
console.log('audio is waiting for more data.');
};
if (mediaSession) {
mediaSession.setActionHandler('play', () => {
store.dispatch("player/play");
});
store.dispatch('player/play')
})
mediaSession.setActionHandler('pause', () => {
store.dispatch("player/pause");
});
store.dispatch('player/pause')
})
mediaSession.setActionHandler('nexttrack', () => {
store.dispatch("player/next");
});
store.dispatch('player/next')
})
mediaSession.setActionHandler('previoustrack', () => {
store.dispatch("player/previous");
});
store.dispatch('player/previous')
})
mediaSession.setActionHandler('stop', () => {
store.dispatch("player/pause");
});
mediaSession.setActionHandler("seekto", (details) => {
store.commit("player/setPosition", details.seekTime);
});
mediaSession.setActionHandler("seekforward", (details) => {
const offset = details.seekOffset || 10;
store.commit("player/setPosition", Math.min(audio.currentTime + offset, audio.duration));
});
mediaSession.setActionHandler("seekbackward", (details) => {
const offset = details.seekOffset || 10;
store.commit("player/setPosition", Math.max(audio.currentTime - offset, 0));
});
store.dispatch('player/pause')
})
mediaSession.setActionHandler('seekto', (details) => {
store.commit('player/setPosition', details.seekTime)
})
mediaSession.setActionHandler('seekforward', (details) => {
const offset = details.seekOffset || 10
store.commit('player/setPosition', Math.min(audio.currentTime + offset, audio.duration))
})
mediaSession.setActionHandler('seekbackward', (details) => {
const offset = details.seekOffset || 10
store.commit('player/setPosition', Math.max(audio.currentTime - offset, 0))
})
// FIXME
// function updatePositionState() {
// if (mediaSession && mediaSession.setPositionState) {

View File

@ -1,9 +1,9 @@
<template>
<Spinner :data="playlist" v-slot="{ data }">
<Spinner v-slot="{ data }" :data="playlist">
<div class="d-flex justify-content-between">
<h1>{{ data.name }}</h1>
<OverflowMenu>
<b-dropdown-item-btn @click="deletePlaylist()" variant="danger">
<b-dropdown-item-btn variant="danger" @click="deletePlaylist()">
Delete playlist
</b-dropdown-item-btn>
</OverflowMenu>
@ -18,43 +18,42 @@
</Spinner>
</template>
<script lang="ts">
import Vue from "vue";
import TrackList from "@/library/TrackList.vue";
import { mapActions } from 'vuex';
import Vue from 'vue'
import TrackList from '@/library/TrackList.vue'
export default Vue.extend({
components: {
TrackList,
},
props: {
id: String,
},
data() {
return {
playlist: null as any,
};
},
watch: {
'id': {
immediate: true,
handler(value: string) {
this.playlist = null;
this.$api.getPlaylist(value).then(playlist => {
this.playlist = playlist;//.sort((a: any, b:any) => a.created.localeCompare(b.created));
});
export default Vue.extend({
components: {
TrackList,
},
props: {
id: { type: String, required: true }
},
data() {
return {
playlist: null as any,
}
},
watch: {
id: {
immediate: true,
handler(value: string) {
this.playlist = null
this.$api.getPlaylist(value).then(playlist => {
this.playlist = playlist// .sort((a: any, b:any) => a.created.localeCompare(b.created));
})
}
}
},
methods: {
remove(index: number) {
this.playlist.tracks.splice(index, 1)
return this.$api.removeFromPlaylist(this.id, index.toString())
},
deletePlaylist() {
return this.$store.dispatch('deletePlaylist', this.id).then(() => {
this.$router.replace({ name: 'playlists' })
})
},
}
},
methods: {
remove(index: number) {
this.playlist.tracks.splice(index, 1);
return this.$api.removeFromPlaylist(this.id, index.toString());
},
deletePlaylist() {
return this.$store.dispatch("deletePlaylist", this.id).then(() => {
this.$router.replace({name: "playlists"})
})
},
}
});
})
</script>

View File

@ -1,9 +1,9 @@
<template>
<Tiles square>
<Tile v-for="item in playlists" :key="item.id"
:image="item.image"
:to="{name: 'playlist', params: { id: item.id } }"
:title="item.name">
:image="item.image"
:to="{name: 'playlist', params: { id: item.id } }"
:title="item.name">
<template v-slot:text>
<strong>{{ item.songCount }}</strong> songs
</template>
@ -11,13 +11,13 @@
</Tiles>
</template>
<script lang="ts">
import Vue from "vue";
import { mapState, mapActions } from 'vuex';
import Vue from 'vue'
import { mapState, mapActions } from 'vuex'
export default Vue.extend({
computed: {
...mapState([
"playlists"
'playlists'
]),
},
methods: {
@ -25,5 +25,5 @@
deletePlaylist: 'deletePlaylist'
})
}
});
})
</script>

View File

@ -3,21 +3,21 @@
<span class="nav-link">
<small class="text-uppercase text-muted font-weight-bold">
Playlists
<button class="btn btn-link btn-sm p-0 float-right" @click="showModal = true">
<Icon icon="plus"/>
</button>
<button class="btn btn-link btn-sm p-0 float-right" @click="showModal = true">
<Icon icon="plus" />
</button>
</small>
</span>
<router-link class="nav-item nav-link" :to="{name: 'playlist', params: { id: 'random' }}">
<Icon icon="music-note-list" class="mr-2"/> Random
<Icon icon="music-note-list" class="mr-2" /> Random
</router-link>
<router-link v-for="item in playlists" :key="item.id"
:to="{name: 'playlist', params: { id: item.id }}"
class="nav-item nav-link">
:to="{name: 'playlist', params: { id: item.id }}"
class="nav-item nav-link">
<span @dragover="onDragover" @drop="onDrop(item.id, $event)">
<Icon icon="music-note-list" class="mr-2"/> {{ item.name }}
<Icon icon="music-note-list" class="mr-2" /> {{ item.name }}
</span>
</router-link>
@ -27,47 +27,48 @@
<b-modal v-model="showModal" title="New playlist">
<b-form-group label="Name">
<b-form-input type="text" v-model="playlistName"/>
<b-form-input v-model="playlistName" type="text" />
</b-form-group>
<template #modal-footer>
<b-button variant="primary" @click="createPlaylist">Create</b-button>
<b-button variant="primary" @click="createPlaylist">
Create
</b-button>
</template>
</b-modal>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import { mapState } from 'vuex';
import Vue from 'vue'
export default Vue.extend({
data() {
return {
playlistName: "",
showModal: false,
export default Vue.extend({
data() {
return {
playlistName: '',
showModal: false,
}
},
computed: {
playlists() {
return this.$store.state.playlists.slice(0, 10)
},
},
methods: {
createPlaylist() {
const name = this.playlistName
this.playlistName = ''
this.showModal = false
return this.$store.dispatch('createPlaylist', name)
},
onDrop(playlistId: string, event: any) {
console.log('onDrop')
event.preventDefault()
const trackId = event.dataTransfer.getData('id')
return this.$store.dispatch('addTrackToPlaylist', { playlistId, trackId })
},
onDragover(event: any) {
console.log('onDragover')
event.preventDefault()
},
}
},
computed: {
playlists() {
return this.$store.state.playlists.slice(0, 10);
},
},
methods: {
createPlaylist() {
const name = this.playlistName;
this.playlistName = "";
this.showModal = false;
return this.$store.dispatch("createPlaylist", name);
},
onDrop(playlistId: string, event: any) {
console.log("onDrop")
event.preventDefault();
const trackId = event.dataTransfer.getData("id");
return this.$store.dispatch("addTrackToPlaylist", { playlistId, trackId })
},
onDragover(event: any) {
console.log("onDragover")
event.preventDefault();
},
}
});
})
</script>

View File

@ -1,14 +1,13 @@
<template>
<div v-if="items">
<TrackList :tracks="items" show-album/>
<TrackList :tracks="items" show-album />
<table class="table">
<thead>
</thead>
<thead />
<tbody>
<tr v-for="item in items" :key="item.id">
<td>
<Icon icon="play-fill" @click="() => {}"/>
<Icon icon="plus" @click="() => {}"/>
<Icon icon="play-fill" @click="() => {}" />
<Icon icon="plus" @click="() => {}" />
</td>
<td>{{ item.artist }}</td>
<td>{{ item.album }}</td>
@ -19,24 +18,24 @@
</div>
</template>
<script lang="ts">
import Vue from "vue";
import TrackList from "@/library/TrackList.vue";
import Vue from 'vue'
import TrackList from '@/library/TrackList.vue'
export default Vue.extend({
components: {
TrackList,
},
data() {
return {
loading: true,
items: [] as any[],
};
},
created() {
this.$api.getRandomSongs().then(items => {
this.loading = false;
this.items = items;//.sort((a: any, b:any) => a.created.localeCompare(b.created));
});
}
});
export default Vue.extend({
components: {
TrackList,
},
data() {
return {
loading: true,
items: [] as any[],
}
},
created() {
this.$api.getRandomSongs().then(items => {
this.loading = false
this.items = items// .sort((a: any, b:any) => a.created.localeCompare(b.created));
})
}
})
</script>

View File

@ -2,25 +2,25 @@
<div>
<form class="form-inline my-2 my-lg-0" @submit.prevent="search">
<input
class="form-control mr-sm-2"
type="search" placeholder="Search"
v-model="query">
v-model="query"
class="form-control mr-sm-2" type="search"
placeholder="Search">
</form>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import Vue from 'vue'
export default Vue.extend({
data() {
return {
query: ""
export default Vue.extend({
data() {
return {
query: ''
}
},
methods: {
search(): void {
this.$router.push({ name: 'search', query: { q: this.query } })
}
}
},
methods: {
search(): void {
this.$router.push({ name: 'search', query: { q: this.query } });
}
}
});
})
</script>

View File

@ -2,25 +2,25 @@
<div v-if="result">
<div v-if="result.artists.length > 0" class="mb-5">
<h1>Artists</h1>
<ArtistList :items="result.artists"/>
<ArtistList :items="result.artists" />
</div>
<div v-if="result.albums.length > 0" class="mb-5">
<h1>Albums</h1>
<AlbumList :items="result.albums"/>
<AlbumList :items="result.albums" />
</div>
<div v-if="result.tracks.length > 0">
<h1>Tracks</h1>
<TrackList :tracks="result.tracks" showAlbum/>
<TrackList :tracks="result.tracks" show-album />
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import AlbumList from "@/library/album/AlbumList.vue";
import ArtistList from "@/library/artist/ArtistList.vue";
import TrackList from "@/library/TrackList.vue"
import Vue from 'vue'
import AlbumList from '@/library/album/AlbumList.vue'
import ArtistList from '@/library/artist/ArtistList.vue'
import TrackList from '@/library/TrackList.vue'
export default Vue.extend({
components: {
@ -29,22 +29,22 @@
TrackList,
},
props: {
query: String,
query: { type: String, required: true }
},
data() {
return {
result: null as any,
};
}
},
watch: {
query: {
immediate: true,
handler(value: string) {
this.$api.search(this.query).then(result => {
this.result = result;
});
this.$api.search(value).then(result => {
this.result = result
})
}
}
},
});
})
</script>

View File

@ -1,62 +1,61 @@
import axios, { AxiosRequestConfig, AxiosInstance } from "axios"
import { AuthService } from '@/auth/service';
export type AlbumSort = "alphabeticalByName" | "newest" | "recent" | "frequent" | "random"
import axios, { AxiosRequestConfig, AxiosInstance } from 'axios'
import { AuthService } from '@/auth/service'
export type AlbumSort = 'alphabeticalByName' | 'newest' | 'recent' | 'frequent' | 'random'
export class API {
readonly http: AxiosInstance;
readonly get: (path: string, params?: any) => Promise<any>;
readonly post: (path: string, params?: any) => Promise<any>;
readonly clientName = window.origin || "web";
readonly clientName = window.origin || 'web';
constructor(private auth: AuthService) {
this.http = axios.create({});
this.http = axios.create({})
this.http.interceptors.request.use((config: AxiosRequestConfig) => {
config.params = config.params || {};
config.params = config.params || {}
config.baseURL = this.auth.server
config.params.u = this.auth.username;
config.params.s = this.auth.salt;
config.params.t = this.auth.hash;
config.params.c = this.clientName;
config.params.f = "json";
config.params.v = "1.15.0";
return config;
});
config.params.u = this.auth.username
config.params.s = this.auth.salt
config.params.t = this.auth.hash
config.params.c = this.clientName
config.params.f = 'json'
config.params.v = '1.15.0'
return config
})
this.get = (path: string, params: any = {}) => {
return this.http.get(path, {params}).then(response => {
const subsonicResponse = response.data["subsonic-response"];
if (subsonicResponse.status !== "ok") {
const message = subsonicResponse.error?.message || subsonicResponse.status;
const err = new Error(message);
return Promise.reject(err);
}
return Promise.resolve(subsonicResponse);
return this.http.get(path, { params }).then(response => {
const subsonicResponse = response.data['subsonic-response']
if (subsonicResponse.status !== 'ok') {
const message = subsonicResponse.error?.message || subsonicResponse.status
const err = new Error(message)
return Promise.reject(err)
}
return Promise.resolve(subsonicResponse)
})
}
this.post = (path: string, params: any = {}) => {
return this.http.post(path, params).then(response => {
const subsonicResponse = response.data["subsonic-response"];
if (subsonicResponse.status !== "ok") {
const err = new Error(subsonicResponse.status);
return Promise.reject(err);
}
return Promise.resolve(subsonicResponse);
const subsonicResponse = response.data['subsonic-response']
if (subsonicResponse.status !== 'ok') {
const err = new Error(subsonicResponse.status)
return Promise.reject(err)
}
return Promise.resolve(subsonicResponse)
})
}
}
async getGenres() {
const response = await this.get("rest/getGenres", {});
const response = await this.get('rest/getGenres', {})
return response.genres.genre
.map((item: any) => ({
id: encodeURIComponent(item.value),
name: item.value,
...item,
}))
.sort((a: any, b:any) => a.name.localeCompare(b.name));;
.sort((a: any, b:any) => a.name.localeCompare(b.name))
}
async getGenreDetails(id: string) {
@ -65,7 +64,7 @@ export class API {
count: 500,
offset: 0,
}
const response = await this.get("rest/getSongsByGenre", params);
const response = await this.get('rest/getSongsByGenre', params)
return {
name: id,
tracks: this.normalizeTrackList(response.songsByGenre.song),
@ -73,37 +72,37 @@ export class API {
}
async getArtists() {
const data = await this.get("rest/getArtists");
const data = await this.get('rest/getArtists')
return data.artists.index.flatMap((index: any) => index.artist.map((artist: any) => ({
id: artist.id,
name: artist.name,
...artist
})));
})))
}
async getAlbums(sort: AlbumSort, size: number = 500) {
async getAlbums(sort: AlbumSort, size = 500) {
const params = {
type: sort,
offset: "0",
offset: '0',
size: size,
};
const data = await this.get("rest/getAlbumList2", params);
}
const data = await this.get('rest/getAlbumList2', params)
return data.albumList2.album.map((item: any) => ({
...item,
image: item.coverArt ? this.getCoverArtUrl(item) : undefined,
}));
}))
}
async getArtistDetails(id: string) {
const params = { id };
const params = { id }
const [info1, info2] = await Promise.all([
this.get("rest/getArtist", params).then(r => r.artist),
this.get("rest/getArtistInfo2", params).then(r => r.artistInfo2),
this.get('rest/getArtist', params).then(r => r.artist),
this.get('rest/getArtistInfo2', params).then(r => r.artistInfo2),
])
return {
id: info1.id,
name: info1.name,
description: (info2.biography || "").replace(/<a[^>]*>.*?<\/a>/gm, ''),
description: (info2.biography || '').replace(/<a[^>]*>.*?<\/a>/gm, ''),
image: info2.largeImageUrl || info2.mediumImageUrl || info2.smallImageUrl,
lastFmUrl: info2.lastFmUrl,
musicBrainzUrl: info2.musicBrainzId
@ -114,14 +113,14 @@ export class API {
name: artist.name,
...artist
}))
};
}
}
async getAlbumDetails(id: string) {
const params = {id};
const data = await this.get("rest/getAlbum", params);
const item = data.album;
const image = this.getCoverArtUrl(item);
const params = { id }
const data = await this.get('rest/getAlbum', params)
const item = data.album
const image = this.getCoverArtUrl(item)
const trackList = item.song.map((s: any) => ({
...s,
image,
@ -131,16 +130,16 @@ export class API {
...item,
image,
song: trackList,
};
}
}
async getPlaylists() {
const response = await this.get("rest/getPlaylists");
const response = await this.get('rest/getPlaylists')
return response.playlists.playlist.map((playlist: any) => ({
...playlist,
name: playlist.name || "(Unnamed)",
name: playlist.name || '(Unnamed)',
image: playlist.songCount > 0 ? this.getCoverArtUrl(playlist) : undefined,
}));
}))
}
async getPlaylist(id: string) {
@ -149,23 +148,23 @@ export class API {
id,
name: 'Random',
tracks: await this.getRandomSongs(),
};
}
}
const response = await this.get("rest/getPlaylist", { id });
const response = await this.get('rest/getPlaylist', { id })
return {
...response.playlist,
name: response.playlist.name || "(Unnamed)",
name: response.playlist.name || '(Unnamed)',
tracks: this.normalizeTrackList(response.playlist.entry || []),
};
}
}
async createPlaylist(name: string) {
await this.get("rest/createPlaylist", { name });
return this.getPlaylists();
await this.get('rest/createPlaylist', { name })
return this.getPlaylists()
}
async deletePlaylist(id: string) {
await this.get("rest/deletePlaylist", { id });
await this.get('rest/deletePlaylist', { id })
}
async addToPlaylist(playlistId: string, trackId: string) {
@ -173,7 +172,7 @@ export class API {
playlistId,
songIdToAdd: trackId,
}
await this.get("rest/updatePlaylist", params);
await this.get('rest/updatePlaylist', params)
}
async removeFromPlaylist(playlistId: string, index: string) {
@ -181,20 +180,20 @@ export class API {
playlistId,
songIndexToRemove: index,
}
await this.get("rest/updatePlaylist", params);
await this.get('rest/updatePlaylist', params)
}
async getRandomSongs() {
const params = {
size: 200,
};
const data = await this.get("rest/getRandomSongs", params);
return this.normalizeTrackList(data.randomSongs.song);
}
const data = await this.get('rest/getRandomSongs', params)
return this.normalizeTrackList(data.randomSongs.song)
}
async getStarred() {
return this
.get("rest/getStarred2")
.get('rest/getStarred2')
.then(r => this.normalizeTrackList(r.starred2.song))
}
@ -204,7 +203,7 @@ export class API {
albumId: type === 'album' ? id : undefined,
artistId: type === 'artist' ? id : undefined,
}
await this.get("rest/star", params);
await this.get('rest/star', params)
}
async unstar(type: 'track' | 'album' | 'artist', id: string) {
@ -213,27 +212,27 @@ export class API {
albumId: type === 'album' ? id : undefined,
artistId: type === 'artist' ? id : undefined,
}
await this.get("rest/unstar", params);
await this.get('rest/unstar', params)
}
async search(query: string) {
const params = {
query,
};
const data = await this.get("rest/search3", params);
}
const data = await this.get('rest/search3', params)
return {
tracks: this.normalizeTrackList(data.searchResult3.song || []),
albums: (data.searchResult3.album || []).map((x: any) => this.normalizeAlbumResponse(x)),
artists: (data.searchResult3.artist || []).map((x: any) => this.normalizeArtistResponse(x)),
};
}
}
private normalizeTrackList(items: any[]) {
return items.map((item => ({
return items.map(item => ({
...item,
url: this.getStreamUrl(item.id),
image: this.getCoverArtUrl(item),
})))
}))
}
private normalizeAlbumResponse(item: any) {
@ -252,14 +251,29 @@ export class API {
private getCoverArtUrl(item: any) {
if (!item.coverArt) {
return undefined;
return undefined
}
const { server, username, salt, hash } = this.auth;
return `${server}/rest/getCoverArt?id=${item.coverArt}&v=1.15.0&u=${username}&s=${salt}&t=${hash}&c=${this.clientName}&size=300`
const { server, username, salt, hash } = this.auth
return `${server}/rest/getCoverArt` +
`?id=${item.coverArt}` +
'&v=1.15.0' +
`&u=${username}` +
`&s=${salt}` +
`&t=${hash}` +
`&c=${this.clientName}` +
'&size=300'
}
private getStreamUrl(id: any) {
const { server, username, salt, hash } = this.auth;
return `${server}/rest/stream?id=${id}&format=raw&v=1.15.0&u=${username}&s=${salt}&t=${hash}&c=${this.clientName}&size=300`
const { server, username, salt, hash } = this.auth
return `${server}/rest/stream` +
`?id=${id}` +
'&format=raw' +
'&v=1.15.0' +
`&u=${username}` +
`&s=${salt}` +
`&t=${hash}` +
`&c=${this.clientName}` +
'&size=300'
}
}

View File

@ -1,8 +1,8 @@
<template functional>
<a :href="props.href"
target="_blank"
rel="noopener noreferrer"
:class="[data.class, data.staticClass]">
<slot/>
target="_blank"
rel="noopener noreferrer"
:class="[data.class, data.staticClass]">
<slot />
</a>
</template>
</template>

View File

@ -1,5 +1,5 @@
<template>
<BIcon :icon="icon" v-bind="$attrs"/>
<BIcon :icon="icon" v-bind="$attrs" />
</template>
<script lang="ts">
import Vue from 'vue'
@ -42,7 +42,7 @@
BIconPersonCircle,
},
props: {
icon: { type: String }
icon: { type: String, required: true }
},
})
</script>

View File

@ -3,7 +3,7 @@
<div class="fixed-img-inner">
<img v-if="props.src" :src="props.src">
<div v-else class="text-muted">
<Icon icon="music-note-beamed"/>
<Icon icon="music-note-beamed" />
</div>
</div>
</div>
@ -18,7 +18,7 @@
.fixed-img {
position: relative;
width: 100%;
.tile-img-inner {
position: absolute;
width: 100%;
@ -42,11 +42,11 @@
}
</style>
<script lang="ts">
import Vue from "vue";
import Vue from 'vue'
export default Vue.extend({
props: {
square: { type: Boolean, default: false },
}
});
</script>
})
</script>

View File

@ -1,13 +1,13 @@
<template>
<b-dropdown variant="link" boundary="window" no-caret toggle-class="p-0">
<template #button-content>
<Icon icon="three-dots-vertical"/>
<Icon icon="three-dots-vertical" />
</template>
<slot/>
<slot />
</b-dropdown>
</template>
<script lang="ts">
import Vue from "vue";
import Vue from 'vue'
export default Vue.extend({});
</script>
export default Vue.extend({})
</script>

View File

@ -1,12 +1,12 @@
<template functional>
<div>
<slot v-if="props.data" :data="props.data"></slot>
<slot v-if="props.data" :data="props.data" />
<div v-else class="text-center">
<span class="spinner-grow"/>
<span class="spinner-grow" />
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import Vue from 'vue'
export default Vue.extend({})
</script>
</script>

View File

@ -2,7 +2,7 @@
<div class="tile card">
<router-link class="tile-img" :to="props.to">
<img v-if="props.image" :src="props.image">
<Icon v-else class="tile-img-fallback text-muted" icon="music-note-beamed"/>
<Icon v-else class="tile-img-fallback text-muted" icon="music-note-beamed" />
</router-link>
<div class="card-body">
<div class="text-truncate font-weight-bold">

View File

@ -1,6 +1,6 @@
<template functional>
<div class="tiles" :class="props.square ? 'tiles-square' : 'tiles-rect'">
<slot></slot>
<slot />
</div>
</template>
<style lang="scss">
@ -29,11 +29,11 @@
}
</style>
<script lang="ts">
import Vue from "vue";
import Vue from 'vue'
export default Vue.extend({
props: {
square: { type: Boolean, default: false },
}
});
</script>
})
</script>

View File

@ -1,10 +1,10 @@
import Vue from 'vue';
import ExternalLink from "./ExternalLink.vue";
import Icon from "./Icon.vue";
import OverflowMenu from "./OverflowMenu.vue";
import Spinner from "./Spinner.vue";
import Tiles from "./Tiles.vue";
import Tile from "./Tile.vue";
import Vue from 'vue'
import ExternalLink from './ExternalLink.vue'
import Icon from './Icon.vue'
import OverflowMenu from './OverflowMenu.vue'
import Spinner from './Spinner.vue'
import Tiles from './Tiles.vue'
import Tile from './Tile.vue'
const components = {
ExternalLink,
@ -13,11 +13,11 @@ const components = {
Spinner,
Tiles,
Tile,
};
}
type Key = keyof typeof components;
Object.keys(components).forEach((_key) => {
const key = _key as keyof typeof components;
Vue.component(key, components[key]);
});
const key = _key as keyof typeof components
Vue.component(key, components[key])
})

View File

@ -1,12 +1,11 @@
import Vue from 'vue';
import Vue from 'vue'
Vue.filter("duration", (value: number) => {
const minutes = Math.floor(value / 60);
const seconds = Math.floor(value % 60);
return (minutes < 10 ? '0' : '') + minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
Vue.filter('duration', (value: number) => {
const minutes = Math.floor(value / 60)
const seconds = Math.floor(value % 60)
return (minutes < 10 ? '0' : '') + minutes + ':' + (seconds < 10 ? '0' : '') + seconds
})
Vue.filter("dateTime", (value: string) => {
return value;
})
Vue.filter('dateTime', (value: string) => {
return value
})

View File

@ -1,4 +1,3 @@
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/auth/Login.vue'
import Queue from '@/player/Queue.vue'
@ -14,8 +13,7 @@ import Starred from '@/library/starred/Starred.vue'
import Playlist from '@/playlist/Playlist.vue'
import PlaylistList from '@/playlist/PlaylistList.vue'
import SearchResult from '@/search/SearchResult.vue'
import { AuthService } from '@/auth/service';
import { AuthService } from '@/auth/service'
export function setupRouter(auth: AuthService) {
const router = new Router({
@ -104,15 +102,15 @@ export function setupRouter(auth: AuthService) {
})
},
]
});
})
router.beforeEach((to, from, next) => {
if (to.name !== 'login' && !auth.isAuthenticated()) {
next({name: 'login', query: { returnTo: to.fullPath }});
next({ name: 'login', query: { returnTo: to.fullPath } })
} else {
next();
next()
}
});
})
return router;
return router
}

View File

@ -1,10 +1,8 @@
import Vue from 'vue'
import Vuex, { Module } from 'vuex'
import { ActionContext } from "vuex"
import { playerModule } from "@/player/store"
import axios from 'axios';
import { AuthService } from '@/auth/service';
import { API } from './api';
import { playerModule } from '@/player/store'
import { AuthService } from '@/auth/service'
import { API } from './api'
interface State {
isLoggedIn: boolean;
@ -26,48 +24,48 @@ const setupRootModule = (authService: AuthService, api: API): Module<State, any>
},
mutations: {
setError(state, error) {
state.error = error;
state.error = error
},
clearError(state) {
state.error = null;
state.error = null
},
setLoginSuccess(state, { username, server }) {
state.isLoggedIn = true;
state.username = username;
state.server = server;
state.isLoggedIn = true
state.username = username
state.server = server
},
toggleMenu(state) {
state.showMenu = !state.showMenu;
state.showMenu = !state.showMenu
},
setPlaylists(state, playlists: any[]) {
state.playlists = playlists
.sort((a: any, b: any) => b.changed.localeCompare(a.changed));
.sort((a: any, b: any) => b.changed.localeCompare(a.changed))
},
removePlaylist(state, id: string) {
state.playlists = state.playlists.filter(p => p.id !== id);
state.playlists = state.playlists.filter(p => p.id !== id)
},
},
actions: {
loadPlaylists({ commit }) {
api.getPlaylists().then(result => {
commit("setPlaylists", result);
commit('setPlaylists', result)
})
},
createPlaylist({ commit }, name) {
api.createPlaylist(name).then(result => {
commit("setPlaylists", result);
commit('setPlaylists', result)
})
},
addTrackToPlaylist({ }, { playlistId, trackId }) {
api.addToPlaylist(playlistId, trackId);
api.addToPlaylist(playlistId, trackId)
},
deletePlaylist({ commit, state }, id) {
deletePlaylist({ commit }, id) {
api.deletePlaylist(id).then(() => {
commit("removePlaylist", id)
commit('removePlaylist', id)
})
}
},
});
})
export function setupStore(authService: AuthService, api: API) {
const store = new Vuex.Store({
@ -84,9 +82,9 @@ export function setupStore(authService: AuthService, api: API) {
store.watch(
(state) => state.isLoggedIn,
() => {
store.dispatch("loadPlaylists")
store.dispatch('loadPlaylists')
}
);
)
return store;
return store
}

View File

@ -1,13 +1,13 @@
import MD5 from 'md5-es';
import MD5 from 'md5-es'
export function randomString(): string {
let arr = new Uint8Array(16);
window.crypto.getRandomValues(arr);
const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
arr = arr.map(x => validChars.charCodeAt(x % validChars.length));
return String.fromCharCode.apply(null, Array.from(arr));
let arr = new Uint8Array(16)
window.crypto.getRandomValues(arr)
const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
arr = arr.map(x => validChars.charCodeAt(x % validChars.length))
return String.fromCharCode.apply(null, Array.from(arr))
}
export function md5(str: string): string {
return MD5.hash(str);
return MD5.hash(str)
}

View File

@ -1,5 +1,8 @@
module.exports = {
devServer: {
disableHostCheck: true
disableHostCheck: true,
overlay: {
errors: false
}
}
}
}

1602
yarn.lock

File diff suppressed because it is too large Load Diff