add eslint
This commit is contained in:
parent
9f842bcffe
commit
8e0cc715ab
@ -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
38
.eslintrc.js
Normal 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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
22
package.json
22
package.json
@ -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"
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -20,4 +20,4 @@
|
||||
svg {
|
||||
fill: var(--primary);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
17
src/global.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 }}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
32
src/main.ts
32
src/main.ts
@ -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({
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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])
|
||||
})
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
disableHostCheck: true
|
||||
disableHostCheck: true,
|
||||
overlay: {
|
||||
errors: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user