add eslint
This commit is contained in:
parent
9f842bcffe
commit
8e0cc715ab
@ -3,3 +3,4 @@ indent_style = space
|
|||||||
indent_size = 2
|
indent_size = 2
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = 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,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build"
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
@ -19,11 +20,22 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/howler": "^2.2.1",
|
"@types/howler": "^2.2.1",
|
||||||
"@vue/cli-plugin-babel": "^4.4.6",
|
"@typescript-eslint/eslint-plugin": "^3.9.0",
|
||||||
"@vue/cli-plugin-typescript": "^4.4.6",
|
"@typescript-eslint/parser": "^3.9.0",
|
||||||
"@vue/cli-service": "^4.4.6",
|
"@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": "^1.26.10",
|
||||||
"sass-loader": "^9.0.2",
|
"sass-loader": "^9.0.3",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^3.9.7",
|
||||||
"vue-template-compiler": "^2.6.10"
|
"vue-template-compiler": "^2.6.10"
|
||||||
},
|
},
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<div class="main flex-fill">
|
<div class="main flex-fill">
|
||||||
<div class="container-fluid pt-3 pb-3">
|
<div class="container-fluid pt-3 pb-3">
|
||||||
<TopNav />
|
<TopNav />
|
||||||
<router-view></router-view>
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -28,10 +28,10 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ErrorBar from "./ErrorBar.vue";
|
import ErrorBar from './ErrorBar.vue'
|
||||||
import TopNav from "./TopNav.vue";
|
import TopNav from './TopNav.vue'
|
||||||
import Sidebar from "./Sidebar.vue";
|
import Sidebar from './Sidebar.vue'
|
||||||
import Player from "@/player/Player.vue";
|
import Player from '@/player/Player.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -40,5 +40,5 @@
|
|||||||
Sidebar,
|
Sidebar,
|
||||||
Player,
|
Player,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -12,8 +12,8 @@
|
|||||||
</b-alert>
|
</b-alert>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import { mapState, mapMutations } from 'vuex';
|
import { mapState, mapMutations } from 'vuex'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
computed: {
|
computed: {
|
||||||
@ -24,5 +24,5 @@
|
|||||||
'clearError'
|
'clearError'
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
@ -51,10 +51,9 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import Logo from "./Logo.vue";
|
import Logo from './Logo.vue'
|
||||||
import PlaylistNav from "@/playlist/PlaylistNav.vue";
|
import PlaylistNav from '@/playlist/PlaylistNav.vue'
|
||||||
import { mapState } from 'vuex';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -63,9 +62,9 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
logout() {
|
logout() {
|
||||||
this.$auth.logout();
|
this.$auth.logout()
|
||||||
this.$router.go(0);
|
this.$router.go(0)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
@ -5,7 +5,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<b-sidebar
|
<b-sidebar
|
||||||
:visible="showMenu"
|
:visible="showMenu"
|
||||||
@hidden="toggleMenu"
|
|
||||||
class="d-md-none"
|
class="d-md-none"
|
||||||
sidebar-class="elevated"
|
sidebar-class="elevated"
|
||||||
bg-variant=""
|
bg-variant=""
|
||||||
@ -13,6 +12,7 @@
|
|||||||
no-header
|
no-header
|
||||||
backdrop
|
backdrop
|
||||||
backdrop-variant=""
|
backdrop-variant=""
|
||||||
|
@hidden="toggleMenu"
|
||||||
>
|
>
|
||||||
<Nav />
|
<Nav />
|
||||||
</b-sidebar>
|
</b-sidebar>
|
||||||
@ -24,9 +24,9 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import Nav from "./Nav.vue";
|
import Nav from './Nav.vue'
|
||||||
import { mapState, mapMutations } from 'vuex';
|
import { mapState, mapMutations } from 'vuex'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -38,5 +38,5 @@
|
|||||||
methods: {
|
methods: {
|
||||||
...mapMutations(['toggleMenu']),
|
...mapMutations(['toggleMenu']),
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
@ -4,7 +4,7 @@
|
|||||||
<Icon icon="list" />
|
<Icon icon="list" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="ml-auto"></div>
|
<div class="ml-auto" />
|
||||||
|
|
||||||
<SearchForm />
|
<SearchForm />
|
||||||
|
|
||||||
@ -30,9 +30,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import { mapMutations, mapState } from 'vuex';
|
import { mapMutations, mapState } from 'vuex'
|
||||||
import SearchForm from '@/search/SearchForm.vue';
|
import SearchForm from '@/search/SearchForm.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -40,8 +40,8 @@
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([
|
...mapState([
|
||||||
"server",
|
'server',
|
||||||
"username",
|
'username',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -49,9 +49,9 @@
|
|||||||
'toggleMenu',
|
'toggleMenu',
|
||||||
]),
|
]),
|
||||||
logout() {
|
logout() {
|
||||||
this.$auth.logout();
|
this.$auth.logout()
|
||||||
this.$router.go(0);
|
this.$router.go(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
@ -6,20 +6,20 @@
|
|||||||
<Icon icon="person-circle" />
|
<Icon icon="person-circle" />
|
||||||
</div>
|
</div>
|
||||||
<b-form-group label="Server">
|
<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>
|
||||||
<b-form-group label="Username">
|
<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>
|
||||||
<b-form-group label="Password">
|
<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-form-group>
|
||||||
<b-alert :show="error != null" variant="danger">
|
<b-alert :show="error != null" variant="danger">
|
||||||
<template v-if="error != null">
|
<template v-if="error != null">
|
||||||
Could not log in. ({{ error.message }})
|
Could not log in. ({{ error.message }})
|
||||||
</template>
|
</template>
|
||||||
</b-alert>
|
</b-alert>
|
||||||
<button class="btn btn-primary btn-block" @click="login" :disabled="busy">
|
<button class="btn btn-primary btn-block" :disabled="busy" @click="login">
|
||||||
<b-spinner v-show="busy" small type="grow" /> Log in
|
<b-spinner v-show="busy" small type="grow" /> Log in
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@ -27,8 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>>
|
</template>>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import { mapMutations, mapState } from "vuex";
|
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: {
|
props: {
|
||||||
@ -36,27 +35,13 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
server: "",
|
server: '',
|
||||||
username: "",
|
username: '',
|
||||||
password: "",
|
password: '',
|
||||||
rememberLogin: true,
|
rememberLogin: true,
|
||||||
busy: false,
|
busy: false,
|
||||||
error: null,
|
error: null,
|
||||||
showModal: false,
|
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: {
|
computed: {
|
||||||
@ -64,25 +49,39 @@ export default Vue.extend({
|
|||||||
return this.error == null ? null : false
|
return this.error == null ? null : false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
async created() {
|
||||||
login() {
|
this.server = await this.$auth.server
|
||||||
this.error = null;
|
this.username = await this.$auth.username
|
||||||
this.busy = true;
|
const success = await this.$auth.autoLogin()
|
||||||
this.$auth.loginWithPassword(this.server, this.username, this.password, this.rememberLogin)
|
if (success) {
|
||||||
.then(() => {
|
this.$store.commit('setLoginSuccess', {
|
||||||
this.$store.commit("setLoginSuccess", {
|
|
||||||
username: this.username,
|
username: this.username,
|
||||||
server: this.server,
|
server: this.server,
|
||||||
});
|
})
|
||||||
this.$router.replace(this.returnTo);
|
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 => {
|
.catch(err => {
|
||||||
this.error = err;
|
this.error = err
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.busy = false;
|
this.busy = false
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,69 +1,74 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios'
|
||||||
import { randomString, md5 } from '@/shared/utils';
|
import { randomString, md5 } from '@/shared/utils'
|
||||||
|
|
||||||
|
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
public server: string = "";
|
public server = '';
|
||||||
public username: string = "";
|
public username = '';
|
||||||
public salt: string = "";
|
public salt = '';
|
||||||
public hash: string = "";
|
public hash = '';
|
||||||
private authenticated: boolean = false;
|
private authenticated = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.server = localStorage.getItem("server") || "/api";
|
this.server = localStorage.getItem('server') || '/api'
|
||||||
this.username = localStorage.getItem("username") || "guest1";
|
this.username = localStorage.getItem('username') || 'guest1'
|
||||||
this.salt = localStorage.getItem("salt") || "";
|
this.salt = localStorage.getItem('salt') || ''
|
||||||
this.hash = localStorage.getItem("hash") || "";
|
this.hash = localStorage.getItem('hash') || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveSession() {
|
private saveSession() {
|
||||||
localStorage.setItem("server", this.server);
|
localStorage.setItem('server', this.server)
|
||||||
localStorage.setItem("username", this.username);
|
localStorage.setItem('username', this.username)
|
||||||
localStorage.setItem("salt", this.salt);
|
localStorage.setItem('salt', this.salt)
|
||||||
localStorage.setItem("hash", this.hash);
|
localStorage.setItem('hash', this.hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
async autoLogin(): Promise<boolean> {
|
async autoLogin(): Promise<boolean> {
|
||||||
if (!this.server || !this.username) {
|
if (!this.server || !this.username) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
return this.loginWithHash(this.server, this.username, this.salt, this.hash, false)
|
return this.loginWithHash(this.server, this.username, this.salt, this.hash, false)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false);
|
.catch(() => false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async loginWithPassword(server: string, username: string, password: string, remember: boolean) {
|
async loginWithPassword(server: string, username: string, password: string, remember: boolean) {
|
||||||
const salt = randomString();
|
const salt = randomString()
|
||||||
const hash = md5(password + salt);
|
const hash = md5(password + salt)
|
||||||
return this.loginWithHash(server, username, salt, hash, remember);
|
return this.loginWithHash(server, username, salt, hash, remember)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loginWithHash(server: string, username: string, salt: string, hash: string, remember: boolean) {
|
private async loginWithHash(
|
||||||
return axios.get(`${server}/rest/ping.view?u=${username}&s=${salt}&t=${hash}&v=1.15.0&c=app&f=json`)
|
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) => {
|
.then((response) => {
|
||||||
const subsonicResponse = response.data["subsonic-response"];
|
const subsonicResponse = response.data['subsonic-response']
|
||||||
if (!subsonicResponse || subsonicResponse.status !== "ok") {
|
if (!subsonicResponse || subsonicResponse.status !== 'ok') {
|
||||||
const err = new Error(subsonicResponse.status);
|
const err = new Error(subsonicResponse.status)
|
||||||
return Promise.reject(err);
|
return Promise.reject(err)
|
||||||
}
|
}
|
||||||
this.authenticated = true;
|
this.authenticated = true
|
||||||
this.server = server;
|
this.server = server
|
||||||
this.username = username;
|
this.username = username
|
||||||
this.salt = salt;
|
this.salt = salt
|
||||||
this.hash = hash;
|
this.hash = hash
|
||||||
if (remember) {
|
if (remember) {
|
||||||
this.saveSession();
|
this.saveSession()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
localStorage.clear();
|
localStorage.clear()
|
||||||
sessionStorage.clear();
|
sessionStorage.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated() {
|
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
|
export default Vue
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "md5-es";
|
declare module 'md5-es';
|
||||||
|
|
||||||
interface Navigator {
|
interface Navigator {
|
||||||
readonly mediaSession?: MediaSession;
|
readonly mediaSession?: MediaSession;
|
||||||
@ -15,7 +15,16 @@ interface Window {
|
|||||||
|
|
||||||
type MediaSessionPlaybackState = 'none' | 'paused' | 'playing';
|
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 {
|
interface MediaSessionActionDetails {
|
||||||
action: MediaSessionAction;
|
action: MediaSessionAction;
|
||||||
@ -33,7 +42,9 @@ interface MediaPositionState {
|
|||||||
interface MediaSession {
|
interface MediaSession {
|
||||||
playbackState: MediaSessionPlaybackState;
|
playbackState: MediaSessionPlaybackState;
|
||||||
metadata: MediaMetadata | null;
|
metadata: MediaMetadata | null;
|
||||||
setActionHandler(action: MediaSessionAction, listener: ((details: MediaSessionActionDetails) => void)): void;
|
setActionHandler(
|
||||||
|
action: MediaSessionAction,
|
||||||
|
listener: ((details: MediaSessionActionDetails) => void)): void;
|
||||||
setPositionState?(arg: MediaPositionState): void;
|
setPositionState?(arg: MediaPositionState): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue'
|
||||||
import AlbumList from "@/library/album/AlbumList.vue";
|
import AlbumList from '@/library/album/AlbumList.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -20,10 +20,10 @@ export default Vue.extend({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
sections: [
|
sections: [
|
||||||
{ name: "Recently played", key: "recent" },
|
{ name: 'Recently played', key: 'recent' },
|
||||||
{ name: "Recently added", key: "newest" },
|
{ name: 'Recently added', key: 'newest' },
|
||||||
{ name: "Most played", key: "frequent" },
|
{ name: 'Most played', key: 'frequent' },
|
||||||
{ name: "Random", key: "random" },
|
{ name: 'Random', key: 'random' },
|
||||||
],
|
],
|
||||||
loading: true as boolean,
|
loading: true as boolean,
|
||||||
recent: [],
|
recent: [],
|
||||||
@ -33,20 +33,20 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
const size = 10;
|
const size = 10
|
||||||
this.$api.getAlbums("recent", size).then(result => {
|
this.$api.getAlbums('recent', size).then(result => {
|
||||||
this.recent = result;
|
this.recent = result
|
||||||
this.loading = false;
|
this.loading = false
|
||||||
});
|
})
|
||||||
this.$api.getAlbums("newest", size).then(result => {
|
this.$api.getAlbums('newest', size).then(result => {
|
||||||
this.newest = result
|
this.newest = result
|
||||||
});
|
})
|
||||||
this.$api.getAlbums("frequent", size).then(result => {
|
this.$api.getAlbums('frequent', size).then(result => {
|
||||||
this.frequent = result
|
this.frequent = result
|
||||||
});
|
})
|
||||||
this.$api.getAlbums("random", 50).then(result => {
|
this.$api.getAlbums('random', 50).then(result => {
|
||||||
this.random = result
|
this.random = result
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -12,11 +12,11 @@
|
|||||||
<b-dropdown-item-button @click="toggleStarred()">
|
<b-dropdown-item-button @click="toggleStarred()">
|
||||||
{{ starred ? 'Unstar' : 'Star' }}
|
{{ starred ? 'Unstar' : 'Star' }}
|
||||||
</b-dropdown-item-button>
|
</b-dropdown-item-button>
|
||||||
<slot :item="track"></slot>
|
<slot :item="track" />
|
||||||
</b-dropdown>
|
</b-dropdown>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: {
|
props: {
|
||||||
@ -30,18 +30,18 @@
|
|||||||
methods: {
|
methods: {
|
||||||
toggleStarred() {
|
toggleStarred() {
|
||||||
if (this.starred) {
|
if (this.starred) {
|
||||||
this.$api.unstar('track', this.track.id);
|
this.$api.unstar('track', this.track.id)
|
||||||
} else {
|
} else {
|
||||||
this.$api.star('track', this.track.id);
|
this.$api.star('track', this.track.id)
|
||||||
}
|
}
|
||||||
this.starred = !this.starred;
|
this.starred = !this.starred
|
||||||
},
|
},
|
||||||
playNext() {
|
playNext() {
|
||||||
return this.$store.dispatch("player/playNext", this.track);
|
return this.$store.dispatch('player/playNext', this.track)
|
||||||
},
|
},
|
||||||
addToQueue() {
|
addToQueue() {
|
||||||
return this.$store.dispatch("player/addToQueue", this.track);
|
return this.$store.dispatch('player/addToQueue', this.track)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,18 +3,28 @@
|
|||||||
<b-table-simple borderless hover>
|
<b-table-simple borderless hover>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="pl-0 pr-0 text-center text-muted"></th>
|
<th class="pl-0 pr-0 text-center text-muted" />
|
||||||
<th class="text-left">Title</th>
|
<th class="text-left">
|
||||||
<th class="text-left d-none d-lg-table-cell">Artist</th>
|
Title
|
||||||
<th class="text-left d-none d-md-table-cell" v-if="showAlbum">Album</th>
|
</th>
|
||||||
<th class="text-right d-none d-md-table-cell">Duration</th>
|
<th class="text-left d-none d-lg-table-cell">
|
||||||
<th class="text-right">Actions</th>
|
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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(item, index) in tracks" :key="index"
|
<tr v-for="(item, index) in tracks" :key="index"
|
||||||
draggable="true" @dragstart="dragstart(item.id, $event)"
|
draggable="true" :class="{'text-primary': item.id === playingTrackId}"
|
||||||
:class="{'text-primary': item.id === playingTrackId}">
|
@dragstart="dragstart(item.id, $event)">
|
||||||
<td class="pl-0 pr-0 text-center text-muted"
|
<td class="pl-0 pr-0 text-center text-muted"
|
||||||
style="min-width: 20px; max-width: 20px;"
|
style="min-width: 20px; max-width: 20px;"
|
||||||
@click="play(index)">
|
@click="play(index)">
|
||||||
@ -42,7 +52,7 @@
|
|||||||
{{ item.artist }}
|
{{ item.artist }}
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-md-table-cell" v-if="showAlbum">
|
<td v-if="showAlbum" class="d-none d-md-table-cell">
|
||||||
<router-link :to="{name: 'album', params: {id: item.albumId}}">
|
<router-link :to="{name: 'album', params: {id: item.albumId}}">
|
||||||
{{ item.album }}
|
{{ item.album }}
|
||||||
</router-link>
|
</router-link>
|
||||||
@ -52,9 +62,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<TrackContextMenu :track="item">
|
<TrackContextMenu :track="item">
|
||||||
<template v-slot="{ item }">
|
<slot name="context-menu" :index="index" :item="item" />
|
||||||
<slot name="context-menu" :index="index" :item="item"></slot>
|
|
||||||
</template>
|
|
||||||
</TrackContextMenu>
|
</TrackContextMenu>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -76,9 +84,9 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import { mapActions, mapMutations, mapGetters, mapState } from 'vuex';
|
import { mapActions, mapGetters, mapState } from 'vuex'
|
||||||
import TrackContextMenu from "@/library/TrackContextMenu.vue"
|
import TrackContextMenu from '@/library/TrackContextMenu.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -89,20 +97,20 @@ export default Vue.extend({
|
|||||||
showAlbum: { type: Boolean, default: false },
|
showAlbum: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState("player", {
|
...mapState('player', {
|
||||||
isPlaying: "isPlaying",
|
isPlaying: 'isPlaying',
|
||||||
}),
|
}),
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
playingTrackId: "player/trackId",
|
playingTrackId: 'player/trackId',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
playPause: "player/playPause",
|
playPause: 'player/playPause',
|
||||||
}),
|
}),
|
||||||
play(index: number) {
|
play(index: number) {
|
||||||
if ((this.tracks as any)[index].id === this.playingTrackId) {
|
if ((this.tracks as any)[index].id === this.playingTrackId) {
|
||||||
return this.$store.dispatch("player/playPause")
|
return this.$store.dispatch('player/playPause')
|
||||||
}
|
}
|
||||||
return this.$store.dispatch('player/playQueue', {
|
return this.$store.dispatch('player/playQueue', {
|
||||||
index,
|
index,
|
||||||
@ -110,9 +118,9 @@ export default Vue.extend({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
dragstart(id: string, event: any) {
|
dragstart(id: string, event: any) {
|
||||||
console.log("dragstart: " + id)
|
console.log('dragstart: ' + id)
|
||||||
event.dataTransfer.setData("id", id);
|
event.dataTransfer.setData('id', id)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
<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">
|
<div class="ml-3 ml-md-4">
|
||||||
<h1>{{ album.name }}</h1>
|
<h1>{{ album.name }}</h1>
|
||||||
<p>by
|
<p>
|
||||||
|
by
|
||||||
<router-link :to="{name: 'artist', params: { id: album.artistId }}">
|
<router-link :to="{name: 'artist', params: { id: album.artistId }}">
|
||||||
{{ album.artist }}
|
{{ album.artist }}
|
||||||
</router-link>
|
</router-link>
|
||||||
@ -26,15 +27,15 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import TrackList from "@/library/TrackList.vue"
|
import TrackList from '@/library/TrackList.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
TrackList,
|
TrackList,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
id: String
|
id: { type: String, required: true }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -42,7 +43,7 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.album = await this.$api.getAlbumDetails(this.id);
|
this.album = await this.$api.getAlbumDetails(this.id)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,34 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Spinner :data="albums" v-slot="{albums: data}">
|
<Spinner v-slot="{ data }" :data="albums">
|
||||||
<AlbumList :items="albums"/>
|
<AlbumList :items="data" />
|
||||||
</Spinner>
|
</Spinner>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import AlbumList from './AlbumList.vue';
|
import AlbumList from './AlbumList.vue'
|
||||||
import { AlbumSort } from '@/shared/api';
|
import { AlbumSort } from '@/shared/api'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
AlbumList,
|
AlbumList,
|
||||||
},
|
},
|
||||||
props: {
|
|
||||||
msg: String
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
sort: "newest",
|
sort: 'newest',
|
||||||
albums: null,
|
albums: null,
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sortOptions() {
|
sortOptions() {
|
||||||
return [
|
return [
|
||||||
{ text: "A-Z", value: "alphabeticalByName" },
|
{ text: 'A-Z', value: 'alphabeticalByName' },
|
||||||
{ text: "Date", value: "newest" },
|
{ text: 'Date', value: 'newest' },
|
||||||
{ text: "frequent", value: "frequent" },
|
{ text: 'frequent', value: 'frequent' },
|
||||||
// { text: "random", value: "random" },
|
// { text: "random", value: "random" },
|
||||||
// { text: "recent", value: "recent" },
|
// { text: "recent", value: "recent" },
|
||||||
// { text: "starred", value: "starred" },
|
// { text: "starred", value: "starred" },
|
||||||
@ -39,12 +36,12 @@ export default Vue.extend({
|
|||||||
sort: {
|
sort: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(value: AlbumSort) {
|
handler(value: AlbumSort) {
|
||||||
this.albums = null;
|
this.albums = null
|
||||||
this.$api.getAlbums(value).then(albums => {
|
this.$api.getAlbums(value).then(albums => {
|
||||||
this.albums = albums;
|
this.albums = albums
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -7,24 +7,31 @@
|
|||||||
<ExternalLink v-if="item.lastFmUrl" :href="item.lastFmUrl" class="btn btn-secondary mr-2">
|
<ExternalLink v-if="item.lastFmUrl" :href="item.lastFmUrl" class="btn btn-secondary mr-2">
|
||||||
Last.fm
|
Last.fm
|
||||||
</ExternalLink>
|
</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
|
MusicBrainz
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="pt-5">Albums</h3>
|
<h3 class="pt-5">
|
||||||
|
Albums
|
||||||
|
</h3>
|
||||||
<AlbumList :items="item.albums" />
|
<AlbumList :items="item.albums" />
|
||||||
|
|
||||||
<template v-if="item.similarArtist.length > 0">
|
<template v-if="item.similarArtist.length > 0">
|
||||||
<h3 class="pt-5">Similar artists</h3>
|
<h3 class="pt-5">
|
||||||
|
Similar artists
|
||||||
|
</h3>
|
||||||
<ArtistList :items="item.similarArtist" />
|
<ArtistList :items="item.similarArtist" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import AlbumList from "@/library/album/AlbumList.vue";
|
import AlbumList from '@/library/album/AlbumList.vue'
|
||||||
import ArtistList from "@/library/artist/ArtistList.vue";
|
import ArtistList from '@/library/artist/ArtistList.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -32,7 +39,7 @@ export default Vue.extend({
|
|||||||
ArtistList,
|
ArtistList,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
id: String
|
id: { type: String, required: true }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -43,10 +50,10 @@ export default Vue.extend({
|
|||||||
id: {
|
id: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
async handler(value: string) {
|
async handler(value: string) {
|
||||||
this.item = null,
|
this.item = null
|
||||||
this.item = await this.$api.getArtistDetails(this.id);
|
this.item = await this.$api.getArtistDetails(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
<ArtistList :items="items" />
|
<ArtistList :items="items" />
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import ArtistList from './ArtistList.vue';
|
import ArtistList from './ArtistList.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -12,12 +12,12 @@
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
items: []
|
items: []
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$api.getArtists().then(items => {
|
this.$api.getArtists().then(items => {
|
||||||
this.items = items;
|
this.items = items
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -5,15 +5,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import TrackList from "@/library/TrackList.vue"
|
import TrackList from '@/library/TrackList.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
TrackList,
|
TrackList,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
id: String
|
id: { type: String, required: true }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -21,7 +21,7 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
this.item = await this.$api.getGenreDetails(this.id);
|
this.item = await this.$api.getGenreDetails(this.id)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -12,19 +12,19 @@
|
|||||||
</Tiles>
|
</Tiles>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {},
|
components: {},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$api.getGenres().then((items) => {
|
this.$api.getGenres().then((items) => {
|
||||||
this.items = items;
|
this.items = items
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
<TrackList v-if="items" :tracks="items" />
|
<TrackList v-if="items" :tracks="items" />
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import TrackList from "@/library/TrackList.vue"
|
import TrackList from '@/library/TrackList.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -16,8 +16,8 @@
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$api.getStarred().then(result => {
|
this.$api.getStarred().then(result => {
|
||||||
this.items = result;
|
this.items = result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
28
src/main.ts
28
src/main.ts
@ -1,6 +1,6 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Router from "vue-router"
|
import Router from 'vue-router'
|
||||||
import Vuex from "vuex"
|
import Vuex from 'vuex'
|
||||||
import { BootstrapVue } from 'bootstrap-vue'
|
import { BootstrapVue } from 'bootstrap-vue'
|
||||||
import '@/style/main.scss'
|
import '@/style/main.scss'
|
||||||
import '@/shared/components'
|
import '@/shared/components'
|
||||||
@ -8,8 +8,8 @@ import '@/shared/filters'
|
|||||||
import App from '@/app/App.vue'
|
import App from '@/app/App.vue'
|
||||||
import { setupRouter } from '@/shared/router'
|
import { setupRouter } from '@/shared/router'
|
||||||
import { setupStore } from '@/shared/store'
|
import { setupStore } from '@/shared/store'
|
||||||
import { API } from '@/shared/api';
|
import { API } from '@/shared/api'
|
||||||
import { AuthService } from '@/auth/service';
|
import { AuthService } from '@/auth/service'
|
||||||
import { setupAudio } from './player/store'
|
import { setupAudio } from './player/store'
|
||||||
|
|
||||||
declare module 'vue/types/vue' {
|
declare module 'vue/types/vue' {
|
||||||
@ -21,20 +21,20 @@ declare module 'vue/types/vue' {
|
|||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
Vue.use(Router)
|
Vue.use(Router)
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex)
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
const authService = new AuthService();
|
const authService = new AuthService()
|
||||||
const api = new API(authService);
|
const api = new API(authService)
|
||||||
const router = setupRouter(authService);
|
const router = setupRouter(authService)
|
||||||
const store = setupStore(authService, api);
|
const store = setupStore(authService, api)
|
||||||
setupAudio(store);
|
setupAudio(store)
|
||||||
|
|
||||||
Vue.prototype.$auth = authService;
|
Vue.prototype.$auth = authService
|
||||||
Vue.prototype.$api = api;
|
Vue.prototype.$api = api
|
||||||
|
|
||||||
Vue.config.errorHandler = (err, vm, info) => {
|
Vue.config.errorHandler = (err) => {
|
||||||
store.commit("setError", err)
|
store.commit('setError', err)
|
||||||
}
|
}
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
<div class="d-flex player">
|
<div class="d-flex player">
|
||||||
<div v-if="track" class="d-none d-sm-block">
|
<div v-if="track" class="d-none d-sm-block">
|
||||||
<router-link :to="{name: 'album', params: {id: track.albumId}}">
|
<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>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-fill">
|
<div class="flex-fill">
|
||||||
<!-- Progress --->
|
<!-- Progress --->
|
||||||
<div class="progress2" @click="seek">
|
<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>
|
||||||
<div class="row d-flex align-items-center p-2 m-0">
|
<div class="row d-flex align-items-center p-2 m-0">
|
||||||
<!-- Track info --->
|
<!-- Track info --->
|
||||||
@ -50,39 +50,39 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import { mapMutations, mapState, mapGetters, mapActions } from 'vuex';
|
import { mapState, mapGetters, mapActions } from 'vuex'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState("player", {
|
...mapState('player', {
|
||||||
isPlaying: (state: any) => state.isPlaying,
|
isPlaying: (state: any) => state.isPlaying,
|
||||||
currentTime: (state: any) => state.currentTime,
|
currentTime: (state: any) => state.currentTime,
|
||||||
duration: (state: any) => state.duration,
|
duration: (state: any) => state.duration,
|
||||||
}),
|
}),
|
||||||
...mapGetters("player", [
|
...mapGetters('player', [
|
||||||
"track",
|
'track',
|
||||||
"progress",
|
'progress',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions("player", [
|
...mapActions('player', [
|
||||||
"playPause",
|
'playPause',
|
||||||
"next",
|
'next',
|
||||||
"previous",
|
'previous',
|
||||||
]),
|
]),
|
||||||
seek(event: any) {
|
seek(event: any) {
|
||||||
if (event.target) {
|
if (event.target) {
|
||||||
const width = event.currentTarget.clientWidth;
|
const width = event.currentTarget.clientWidth
|
||||||
const value = event.offsetX / width;
|
const value = event.offsetX / width
|
||||||
return this.$store.dispatch("player/seek", value);
|
return this.$store.dispatch('player/seek', value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -10,23 +10,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import { mapState, mapMutations, mapActions } from 'vuex';
|
import { mapState, mapMutations } from 'vuex'
|
||||||
import TrackList from "@/library/TrackList.vue";
|
import TrackList from '@/library/TrackList.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
TrackList,
|
TrackList,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState("player", {
|
...mapState('player', {
|
||||||
tracks: (state: any) => state.queue,
|
tracks: (state: any) => state.queue,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations("player", {
|
...mapMutations('player', {
|
||||||
remove: "removeFromQueue",
|
remove: 'removeFromQueue',
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div class="col" d-flex fill-height grow>
|
<div class="col" d-flex fill-height grow>
|
||||||
<h1>{{ track.name }}</h1>
|
<h1>{{ track.name }}</h1>
|
||||||
<div>{{ track.artist }}</div>
|
<div>{{ track.artist }}</div>
|
||||||
<v-card color=blue tile height="100%" width="100%"></v-card>
|
<v-card color="blue" tile height="100%" width="100%" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -18,11 +18,11 @@
|
|||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: {
|
props: {
|
||||||
album: Object
|
album: { type: Object, required: true }
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Store, Module } from 'vuex'
|
import { Store, Module } from 'vuex'
|
||||||
|
|
||||||
const audio = new Audio();
|
const audio = new Audio()
|
||||||
const storedQueue = JSON.parse(localStorage.getItem("queue") || "[]");
|
const storedQueue = JSON.parse(localStorage.getItem('queue') || '[]')
|
||||||
const storedQueueIndex = parseInt(localStorage.getItem("queueIndex") || "-1");
|
const storedQueueIndex = parseInt(localStorage.getItem('queueIndex') || '-1')
|
||||||
if (storedQueueIndex > -1 && storedQueueIndex < storedQueue.length) {
|
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 {
|
interface State {
|
||||||
queue: any[];
|
queue: any[];
|
||||||
@ -28,177 +28,176 @@ export const playerModule: Module<State, any> = {
|
|||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
setPlaying(state) {
|
setPlaying(state) {
|
||||||
state.isPlaying = true;
|
state.isPlaying = true
|
||||||
if (mediaSession) {
|
if (mediaSession) {
|
||||||
mediaSession.playbackState = "playing";
|
mediaSession.playbackState = 'playing'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setPaused(state) {
|
setPaused(state) {
|
||||||
state.isPlaying = false;
|
state.isPlaying = false
|
||||||
if (mediaSession) {
|
if (mediaSession) {
|
||||||
mediaSession.playbackState = "paused";
|
mediaSession.playbackState = 'paused'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setPosition(state, time: number) {
|
setPosition(state, time: number) {
|
||||||
audio.currentTime = time;
|
audio.currentTime = time
|
||||||
},
|
},
|
||||||
setQueue(state, queue) {
|
setQueue(state, queue) {
|
||||||
state.queue = queue;
|
state.queue = queue
|
||||||
state.queueIndex = -1;
|
state.queueIndex = -1
|
||||||
localStorage.setItem("queue", JSON.stringify(queue));
|
localStorage.setItem('queue', JSON.stringify(queue))
|
||||||
},
|
},
|
||||||
setQueueIndex(state, index) {
|
setQueueIndex(state, index) {
|
||||||
if (state.queue.length === 0) {
|
if (state.queue.length === 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
index = Math.max(0, index);
|
index = Math.max(0, index)
|
||||||
index = index < state.queue.length ? index : 0;
|
index = index < state.queue.length ? index : 0
|
||||||
state.queueIndex = index;
|
state.queueIndex = index
|
||||||
localStorage.setItem("queueIndex", index);
|
localStorage.setItem('queueIndex', index)
|
||||||
const track = state.queue[index];
|
const track = state.queue[index]
|
||||||
audio.src = track.url;
|
audio.src = track.url
|
||||||
if (mediaSession) {
|
if (mediaSession) {
|
||||||
mediaSession.metadata = new MediaMetadata({
|
mediaSession.metadata = new MediaMetadata({
|
||||||
title: track.title,
|
title: track.title,
|
||||||
artist: track.artist,
|
artist: track.artist,
|
||||||
album: track.album,
|
album: track.album,
|
||||||
artwork: track.image ? [{ src: track.image, sizes: "300x300" }] : undefined,
|
artwork: track.image ? [{ src: track.image, sizes: '300x300' }] : undefined,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addToQueue(state, track) {
|
addToQueue(state, track) {
|
||||||
state.queue.push(track);
|
state.queue.push(track)
|
||||||
},
|
},
|
||||||
removeFromQueue(state, index) {
|
removeFromQueue(state, index) {
|
||||||
state.queue.splice(index, 1);
|
state.queue.splice(index, 1)
|
||||||
if (index < state.queueIndex) {
|
if (index < state.queueIndex) {
|
||||||
state.queueIndex--;
|
state.queueIndex--
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setNextInQueue(state, track) {
|
setNextInQueue(state, track) {
|
||||||
state.queue.splice(state.queueIndex + 1, 0, track);
|
state.queue.splice(state.queueIndex + 1, 0, track)
|
||||||
},
|
},
|
||||||
setProgress(state, value: any) {
|
setProgress(state, value: any) {
|
||||||
state.currentTime = value;
|
state.currentTime = value
|
||||||
},
|
},
|
||||||
setDuration(state, value: any) {
|
setDuration(state, value: any) {
|
||||||
state.duration = value;
|
state.duration = value
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async playQueue({ commit }, { queue, index }) {
|
async playQueue({ commit }, { queue, index }) {
|
||||||
commit("setQueue", [...queue]);
|
commit('setQueue', [...queue])
|
||||||
commit("setQueueIndex", index);
|
commit('setQueueIndex', index)
|
||||||
commit("setPlaying");
|
commit('setPlaying')
|
||||||
await audio.play();
|
await audio.play()
|
||||||
},
|
},
|
||||||
async play({ commit }) {
|
async play({ commit }) {
|
||||||
commit("setPlaying");
|
commit('setPlaying')
|
||||||
await audio.play();
|
await audio.play()
|
||||||
},
|
},
|
||||||
pause({ commit }) {
|
pause({ commit }) {
|
||||||
audio.pause();
|
audio.pause()
|
||||||
commit("setPaused");
|
commit('setPaused')
|
||||||
},
|
},
|
||||||
async next({ commit, state }) {
|
async next({ commit, state }) {
|
||||||
commit("setQueueIndex", state.queueIndex + 1);
|
commit('setQueueIndex', state.queueIndex + 1)
|
||||||
commit("setPlaying");
|
commit('setPlaying')
|
||||||
await audio.play();
|
await audio.play()
|
||||||
},
|
},
|
||||||
async previous({ commit, state }) {
|
async previous({ commit, state }) {
|
||||||
commit("setQueueIndex", state.queueIndex - 1);
|
commit('setQueueIndex', state.queueIndex - 1)
|
||||||
commit("setPlaying");
|
commit('setPlaying')
|
||||||
await audio.play();
|
await audio.play()
|
||||||
},
|
},
|
||||||
playPause({ state, dispatch }) {
|
playPause({ state, dispatch }) {
|
||||||
if (state.isPlaying) {
|
if (state.isPlaying) {
|
||||||
return dispatch("pause");
|
return dispatch('pause')
|
||||||
}
|
}
|
||||||
return dispatch("play");
|
return dispatch('play')
|
||||||
},
|
},
|
||||||
seek({ commit, state }, value) {
|
seek({ commit, state }, value) {
|
||||||
commit("setPosition", state.duration * value);
|
commit('setPosition', state.duration * value)
|
||||||
},
|
},
|
||||||
playNext({ commit }, track) {
|
playNext({ commit }, track) {
|
||||||
commit("setNextInQueue", track);
|
commit('setNextInQueue', track)
|
||||||
},
|
},
|
||||||
addToQueue({ commit }, track) {
|
addToQueue({ commit }, track) {
|
||||||
commit("addToQueue", track);
|
commit('addToQueue', track)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
track(state) {
|
track(state) {
|
||||||
if (state.queueIndex != -1) {
|
if (state.queueIndex !== -1) {
|
||||||
return state.queue[state.queueIndex];
|
return state.queue[state.queueIndex]
|
||||||
}
|
}
|
||||||
return null;
|
return null
|
||||||
},
|
},
|
||||||
trackId(state, getters): number {
|
trackId(state, getters): number {
|
||||||
return getters.track ? getters.track.id : -1;
|
return getters.track ? getters.track.id : -1
|
||||||
},
|
},
|
||||||
progress(state) {
|
progress(state) {
|
||||||
if (state.currentTime > -1 && state.duration > 0) {
|
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) {
|
hasNext(state) {
|
||||||
return state.queueIndex < state.queue.length - 1;
|
return state.queueIndex < state.queue.length - 1
|
||||||
},
|
},
|
||||||
hasPrevious(state) {
|
hasPrevious(state) {
|
||||||
return state.queueIndex > 0;
|
return state.queueIndex > 0
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
export function setupAudio(store: Store<any>) {
|
export function setupAudio(store: Store<any>) {
|
||||||
audio.ontimeupdate = (event) => {
|
audio.ontimeupdate = () => {
|
||||||
store.commit("player/setProgress", audio.currentTime)
|
store.commit('player/setProgress', audio.currentTime)
|
||||||
};
|
|
||||||
audio.ondurationchange = (event) => {
|
|
||||||
store.commit("player/setDuration", audio.duration);
|
|
||||||
}
|
}
|
||||||
audio.onended = (event) => {
|
audio.ondurationchange = () => {
|
||||||
store.dispatch("player/next");
|
store.commit('player/setDuration', audio.duration)
|
||||||
}
|
}
|
||||||
audio.onerror = (event) => {
|
audio.onended = () => {
|
||||||
store.commit("player/setPaused");
|
store.dispatch('player/next')
|
||||||
store.commit("setError", audio.error);
|
}
|
||||||
|
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) {
|
if (mediaSession) {
|
||||||
mediaSession.setActionHandler('play', () => {
|
mediaSession.setActionHandler('play', () => {
|
||||||
store.dispatch("player/play");
|
store.dispatch('player/play')
|
||||||
});
|
})
|
||||||
mediaSession.setActionHandler('pause', () => {
|
mediaSession.setActionHandler('pause', () => {
|
||||||
store.dispatch("player/pause");
|
store.dispatch('player/pause')
|
||||||
});
|
})
|
||||||
mediaSession.setActionHandler('nexttrack', () => {
|
mediaSession.setActionHandler('nexttrack', () => {
|
||||||
store.dispatch("player/next");
|
store.dispatch('player/next')
|
||||||
});
|
})
|
||||||
mediaSession.setActionHandler('previoustrack', () => {
|
mediaSession.setActionHandler('previoustrack', () => {
|
||||||
store.dispatch("player/previous");
|
store.dispatch('player/previous')
|
||||||
});
|
})
|
||||||
mediaSession.setActionHandler('stop', () => {
|
mediaSession.setActionHandler('stop', () => {
|
||||||
store.dispatch("player/pause");
|
store.dispatch('player/pause')
|
||||||
});
|
})
|
||||||
mediaSession.setActionHandler("seekto", (details) => {
|
mediaSession.setActionHandler('seekto', (details) => {
|
||||||
store.commit("player/setPosition", details.seekTime);
|
store.commit('player/setPosition', details.seekTime)
|
||||||
});
|
})
|
||||||
mediaSession.setActionHandler("seekforward", (details) => {
|
mediaSession.setActionHandler('seekforward', (details) => {
|
||||||
const offset = details.seekOffset || 10;
|
const offset = details.seekOffset || 10
|
||||||
store.commit("player/setPosition", Math.min(audio.currentTime + offset, audio.duration));
|
store.commit('player/setPosition', Math.min(audio.currentTime + offset, audio.duration))
|
||||||
});
|
})
|
||||||
mediaSession.setActionHandler("seekbackward", (details) => {
|
mediaSession.setActionHandler('seekbackward', (details) => {
|
||||||
const offset = details.seekOffset || 10;
|
const offset = details.seekOffset || 10
|
||||||
store.commit("player/setPosition", Math.max(audio.currentTime - offset, 0));
|
store.commit('player/setPosition', Math.max(audio.currentTime - offset, 0))
|
||||||
});
|
})
|
||||||
// FIXME
|
// FIXME
|
||||||
// function updatePositionState() {
|
// function updatePositionState() {
|
||||||
// if (mediaSession && mediaSession.setPositionState) {
|
// if (mediaSession && mediaSession.setPositionState) {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<Spinner :data="playlist" v-slot="{ data }">
|
<Spinner v-slot="{ data }" :data="playlist">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<h1>{{ data.name }}</h1>
|
<h1>{{ data.name }}</h1>
|
||||||
<OverflowMenu>
|
<OverflowMenu>
|
||||||
<b-dropdown-item-btn @click="deletePlaylist()" variant="danger">
|
<b-dropdown-item-btn variant="danger" @click="deletePlaylist()">
|
||||||
Delete playlist
|
Delete playlist
|
||||||
</b-dropdown-item-btn>
|
</b-dropdown-item-btn>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
@ -18,43 +18,42 @@
|
|||||||
</Spinner>
|
</Spinner>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import TrackList from "@/library/TrackList.vue";
|
import TrackList from '@/library/TrackList.vue'
|
||||||
import { mapActions } from 'vuex';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
TrackList,
|
TrackList,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
id: String,
|
id: { type: String, required: true }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
playlist: null as any,
|
playlist: null as any,
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'id': {
|
id: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(value: string) {
|
handler(value: string) {
|
||||||
this.playlist = null;
|
this.playlist = null
|
||||||
this.$api.getPlaylist(value).then(playlist => {
|
this.$api.getPlaylist(value).then(playlist => {
|
||||||
this.playlist = playlist;//.sort((a: any, b:any) => a.created.localeCompare(b.created));
|
this.playlist = playlist// .sort((a: any, b:any) => a.created.localeCompare(b.created));
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
remove(index: number) {
|
remove(index: number) {
|
||||||
this.playlist.tracks.splice(index, 1);
|
this.playlist.tracks.splice(index, 1)
|
||||||
return this.$api.removeFromPlaylist(this.id, index.toString());
|
return this.$api.removeFromPlaylist(this.id, index.toString())
|
||||||
},
|
},
|
||||||
deletePlaylist() {
|
deletePlaylist() {
|
||||||
return this.$store.dispatch("deletePlaylist", this.id).then(() => {
|
return this.$store.dispatch('deletePlaylist', this.id).then(() => {
|
||||||
this.$router.replace({name: "playlists"})
|
this.$router.replace({ name: 'playlists' })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -11,13 +11,13 @@
|
|||||||
</Tiles>
|
</Tiles>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import { mapState, mapActions } from 'vuex';
|
import { mapState, mapActions } from 'vuex'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([
|
...mapState([
|
||||||
"playlists"
|
'playlists'
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -25,5 +25,5 @@
|
|||||||
deletePlaylist: 'deletePlaylist'
|
deletePlaylist: 'deletePlaylist'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -27,47 +27,48 @@
|
|||||||
|
|
||||||
<b-modal v-model="showModal" title="New playlist">
|
<b-modal v-model="showModal" title="New playlist">
|
||||||
<b-form-group label="Name">
|
<b-form-group label="Name">
|
||||||
<b-form-input type="text" v-model="playlistName"/>
|
<b-form-input v-model="playlistName" type="text" />
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<template #modal-footer>
|
<template #modal-footer>
|
||||||
<b-button variant="primary" @click="createPlaylist">Create</b-button>
|
<b-button variant="primary" @click="createPlaylist">
|
||||||
|
Create
|
||||||
|
</b-button>
|
||||||
</template>
|
</template>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import { mapState } from 'vuex';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
playlistName: "",
|
playlistName: '',
|
||||||
showModal: false,
|
showModal: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
playlists() {
|
playlists() {
|
||||||
return this.$store.state.playlists.slice(0, 10);
|
return this.$store.state.playlists.slice(0, 10)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
createPlaylist() {
|
createPlaylist() {
|
||||||
const name = this.playlistName;
|
const name = this.playlistName
|
||||||
this.playlistName = "";
|
this.playlistName = ''
|
||||||
this.showModal = false;
|
this.showModal = false
|
||||||
return this.$store.dispatch("createPlaylist", name);
|
return this.$store.dispatch('createPlaylist', name)
|
||||||
},
|
},
|
||||||
onDrop(playlistId: string, event: any) {
|
onDrop(playlistId: string, event: any) {
|
||||||
console.log("onDrop")
|
console.log('onDrop')
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
const trackId = event.dataTransfer.getData("id");
|
const trackId = event.dataTransfer.getData('id')
|
||||||
return this.$store.dispatch("addTrackToPlaylist", { playlistId, trackId })
|
return this.$store.dispatch('addTrackToPlaylist', { playlistId, trackId })
|
||||||
},
|
},
|
||||||
onDragover(event: any) {
|
onDragover(event: any) {
|
||||||
console.log("onDragover")
|
console.log('onDragover')
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
<div v-if="items">
|
<div v-if="items">
|
||||||
<TrackList :tracks="items" show-album />
|
<TrackList :tracks="items" show-album />
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead />
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="item in items" :key="item.id">
|
<tr v-for="item in items" :key="item.id">
|
||||||
<td>
|
<td>
|
||||||
@ -19,8 +18,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import TrackList from "@/library/TrackList.vue";
|
import TrackList from '@/library/TrackList.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -30,13 +29,13 @@ export default Vue.extend({
|
|||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
items: [] as any[],
|
items: [] as any[],
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$api.getRandomSongs().then(items => {
|
this.$api.getRandomSongs().then(items => {
|
||||||
this.loading = false;
|
this.loading = false
|
||||||
this.items = items;//.sort((a: any, b:any) => a.created.localeCompare(b.created));
|
this.items = items// .sort((a: any, b:any) => a.created.localeCompare(b.created));
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -2,25 +2,25 @@
|
|||||||
<div>
|
<div>
|
||||||
<form class="form-inline my-2 my-lg-0" @submit.prevent="search">
|
<form class="form-inline my-2 my-lg-0" @submit.prevent="search">
|
||||||
<input
|
<input
|
||||||
class="form-control mr-sm-2"
|
v-model="query"
|
||||||
type="search" placeholder="Search"
|
class="form-control mr-sm-2" type="search"
|
||||||
v-model="query">
|
placeholder="Search">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
query: ""
|
query: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
search(): void {
|
search(): void {
|
||||||
this.$router.push({ name: 'search', query: { q: this.query } });
|
this.$router.push({ name: 'search', query: { q: this.query } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -12,15 +12,15 @@
|
|||||||
|
|
||||||
<div v-if="result.tracks.length > 0">
|
<div v-if="result.tracks.length > 0">
|
||||||
<h1>Tracks</h1>
|
<h1>Tracks</h1>
|
||||||
<TrackList :tracks="result.tracks" showAlbum/>
|
<TrackList :tracks="result.tracks" show-album />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
import AlbumList from "@/library/album/AlbumList.vue";
|
import AlbumList from '@/library/album/AlbumList.vue'
|
||||||
import ArtistList from "@/library/artist/ArtistList.vue";
|
import ArtistList from '@/library/artist/ArtistList.vue'
|
||||||
import TrackList from "@/library/TrackList.vue"
|
import TrackList from '@/library/TrackList.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -29,22 +29,22 @@
|
|||||||
TrackList,
|
TrackList,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
query: String,
|
query: { type: String, required: true }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
result: null as any,
|
result: null as any,
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
query: {
|
query: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(value: string) {
|
handler(value: string) {
|
||||||
this.$api.search(this.query).then(result => {
|
this.$api.search(value).then(result => {
|
||||||
this.result = result;
|
this.result = result
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,62 +1,61 @@
|
|||||||
import axios, { AxiosRequestConfig, AxiosInstance } from "axios"
|
import axios, { AxiosRequestConfig, AxiosInstance } from 'axios'
|
||||||
import { AuthService } from '@/auth/service';
|
import { AuthService } from '@/auth/service'
|
||||||
|
|
||||||
export type AlbumSort = "alphabeticalByName" | "newest" | "recent" | "frequent" | "random"
|
|
||||||
|
|
||||||
|
export type AlbumSort = 'alphabeticalByName' | 'newest' | 'recent' | 'frequent' | 'random'
|
||||||
|
|
||||||
export class API {
|
export class API {
|
||||||
readonly http: AxiosInstance;
|
readonly http: AxiosInstance;
|
||||||
readonly get: (path: string, params?: any) => Promise<any>;
|
readonly get: (path: string, params?: any) => Promise<any>;
|
||||||
readonly post: (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) {
|
constructor(private auth: AuthService) {
|
||||||
this.http = axios.create({});
|
this.http = axios.create({})
|
||||||
this.http.interceptors.request.use((config: AxiosRequestConfig) => {
|
this.http.interceptors.request.use((config: AxiosRequestConfig) => {
|
||||||
config.params = config.params || {};
|
config.params = config.params || {}
|
||||||
config.baseURL = this.auth.server
|
config.baseURL = this.auth.server
|
||||||
config.params.u = this.auth.username;
|
config.params.u = this.auth.username
|
||||||
config.params.s = this.auth.salt;
|
config.params.s = this.auth.salt
|
||||||
config.params.t = this.auth.hash;
|
config.params.t = this.auth.hash
|
||||||
config.params.c = this.clientName;
|
config.params.c = this.clientName
|
||||||
config.params.f = "json";
|
config.params.f = 'json'
|
||||||
config.params.v = "1.15.0";
|
config.params.v = '1.15.0'
|
||||||
return config;
|
return config
|
||||||
});
|
})
|
||||||
|
|
||||||
this.get = (path: string, params: any = {}) => {
|
this.get = (path: string, params: any = {}) => {
|
||||||
return this.http.get(path, { params }).then(response => {
|
return this.http.get(path, { params }).then(response => {
|
||||||
const subsonicResponse = response.data["subsonic-response"];
|
const subsonicResponse = response.data['subsonic-response']
|
||||||
if (subsonicResponse.status !== "ok") {
|
if (subsonicResponse.status !== 'ok') {
|
||||||
const message = subsonicResponse.error?.message || subsonicResponse.status;
|
const message = subsonicResponse.error?.message || subsonicResponse.status
|
||||||
const err = new Error(message);
|
const err = new Error(message)
|
||||||
return Promise.reject(err);
|
return Promise.reject(err)
|
||||||
}
|
}
|
||||||
return Promise.resolve(subsonicResponse);
|
return Promise.resolve(subsonicResponse)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.post = (path: string, params: any = {}) => {
|
this.post = (path: string, params: any = {}) => {
|
||||||
return this.http.post(path, params).then(response => {
|
return this.http.post(path, params).then(response => {
|
||||||
const subsonicResponse = response.data["subsonic-response"];
|
const subsonicResponse = response.data['subsonic-response']
|
||||||
if (subsonicResponse.status !== "ok") {
|
if (subsonicResponse.status !== 'ok') {
|
||||||
const err = new Error(subsonicResponse.status);
|
const err = new Error(subsonicResponse.status)
|
||||||
return Promise.reject(err);
|
return Promise.reject(err)
|
||||||
}
|
}
|
||||||
return Promise.resolve(subsonicResponse);
|
return Promise.resolve(subsonicResponse)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGenres() {
|
async getGenres() {
|
||||||
const response = await this.get("rest/getGenres", {});
|
const response = await this.get('rest/getGenres', {})
|
||||||
return response.genres.genre
|
return response.genres.genre
|
||||||
.map((item: any) => ({
|
.map((item: any) => ({
|
||||||
id: encodeURIComponent(item.value),
|
id: encodeURIComponent(item.value),
|
||||||
name: item.value,
|
name: item.value,
|
||||||
...item,
|
...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) {
|
async getGenreDetails(id: string) {
|
||||||
@ -65,7 +64,7 @@ export class API {
|
|||||||
count: 500,
|
count: 500,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
}
|
}
|
||||||
const response = await this.get("rest/getSongsByGenre", params);
|
const response = await this.get('rest/getSongsByGenre', params)
|
||||||
return {
|
return {
|
||||||
name: id,
|
name: id,
|
||||||
tracks: this.normalizeTrackList(response.songsByGenre.song),
|
tracks: this.normalizeTrackList(response.songsByGenre.song),
|
||||||
@ -73,37 +72,37 @@ export class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getArtists() {
|
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) => ({
|
return data.artists.index.flatMap((index: any) => index.artist.map((artist: any) => ({
|
||||||
id: artist.id,
|
id: artist.id,
|
||||||
name: artist.name,
|
name: artist.name,
|
||||||
...artist
|
...artist
|
||||||
})));
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAlbums(sort: AlbumSort, size: number = 500) {
|
async getAlbums(sort: AlbumSort, size = 500) {
|
||||||
const params = {
|
const params = {
|
||||||
type: sort,
|
type: sort,
|
||||||
offset: "0",
|
offset: '0',
|
||||||
size: size,
|
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) => ({
|
return data.albumList2.album.map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
image: item.coverArt ? this.getCoverArtUrl(item) : undefined,
|
image: item.coverArt ? this.getCoverArtUrl(item) : undefined,
|
||||||
}));
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getArtistDetails(id: string) {
|
async getArtistDetails(id: string) {
|
||||||
const params = { id };
|
const params = { id }
|
||||||
const [info1, info2] = await Promise.all([
|
const [info1, info2] = await Promise.all([
|
||||||
this.get("rest/getArtist", params).then(r => r.artist),
|
this.get('rest/getArtist', params).then(r => r.artist),
|
||||||
this.get("rest/getArtistInfo2", params).then(r => r.artistInfo2),
|
this.get('rest/getArtistInfo2', params).then(r => r.artistInfo2),
|
||||||
])
|
])
|
||||||
return {
|
return {
|
||||||
id: info1.id,
|
id: info1.id,
|
||||||
name: info1.name,
|
name: info1.name,
|
||||||
description: (info2.biography || "").replace(/<a[^>]*>.*?<\/a>/gm, ''),
|
description: (info2.biography || '').replace(/<a[^>]*>.*?<\/a>/gm, ''),
|
||||||
image: info2.largeImageUrl || info2.mediumImageUrl || info2.smallImageUrl,
|
image: info2.largeImageUrl || info2.mediumImageUrl || info2.smallImageUrl,
|
||||||
lastFmUrl: info2.lastFmUrl,
|
lastFmUrl: info2.lastFmUrl,
|
||||||
musicBrainzUrl: info2.musicBrainzId
|
musicBrainzUrl: info2.musicBrainzId
|
||||||
@ -114,14 +113,14 @@ export class API {
|
|||||||
name: artist.name,
|
name: artist.name,
|
||||||
...artist
|
...artist
|
||||||
}))
|
}))
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAlbumDetails(id: string) {
|
async getAlbumDetails(id: string) {
|
||||||
const params = {id};
|
const params = { id }
|
||||||
const data = await this.get("rest/getAlbum", params);
|
const data = await this.get('rest/getAlbum', params)
|
||||||
const item = data.album;
|
const item = data.album
|
||||||
const image = this.getCoverArtUrl(item);
|
const image = this.getCoverArtUrl(item)
|
||||||
const trackList = item.song.map((s: any) => ({
|
const trackList = item.song.map((s: any) => ({
|
||||||
...s,
|
...s,
|
||||||
image,
|
image,
|
||||||
@ -131,16 +130,16 @@ export class API {
|
|||||||
...item,
|
...item,
|
||||||
image,
|
image,
|
||||||
song: trackList,
|
song: trackList,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlaylists() {
|
async getPlaylists() {
|
||||||
const response = await this.get("rest/getPlaylists");
|
const response = await this.get('rest/getPlaylists')
|
||||||
return response.playlists.playlist.map((playlist: any) => ({
|
return response.playlists.playlist.map((playlist: any) => ({
|
||||||
...playlist,
|
...playlist,
|
||||||
name: playlist.name || "(Unnamed)",
|
name: playlist.name || '(Unnamed)',
|
||||||
image: playlist.songCount > 0 ? this.getCoverArtUrl(playlist) : undefined,
|
image: playlist.songCount > 0 ? this.getCoverArtUrl(playlist) : undefined,
|
||||||
}));
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlaylist(id: string) {
|
async getPlaylist(id: string) {
|
||||||
@ -149,23 +148,23 @@ export class API {
|
|||||||
id,
|
id,
|
||||||
name: 'Random',
|
name: 'Random',
|
||||||
tracks: await this.getRandomSongs(),
|
tracks: await this.getRandomSongs(),
|
||||||
};
|
|
||||||
}
|
}
|
||||||
const response = await this.get("rest/getPlaylist", { id });
|
}
|
||||||
|
const response = await this.get('rest/getPlaylist', { id })
|
||||||
return {
|
return {
|
||||||
...response.playlist,
|
...response.playlist,
|
||||||
name: response.playlist.name || "(Unnamed)",
|
name: response.playlist.name || '(Unnamed)',
|
||||||
tracks: this.normalizeTrackList(response.playlist.entry || []),
|
tracks: this.normalizeTrackList(response.playlist.entry || []),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPlaylist(name: string) {
|
async createPlaylist(name: string) {
|
||||||
await this.get("rest/createPlaylist", { name });
|
await this.get('rest/createPlaylist', { name })
|
||||||
return this.getPlaylists();
|
return this.getPlaylists()
|
||||||
}
|
}
|
||||||
|
|
||||||
async deletePlaylist(id: string) {
|
async deletePlaylist(id: string) {
|
||||||
await this.get("rest/deletePlaylist", { id });
|
await this.get('rest/deletePlaylist', { id })
|
||||||
}
|
}
|
||||||
|
|
||||||
async addToPlaylist(playlistId: string, trackId: string) {
|
async addToPlaylist(playlistId: string, trackId: string) {
|
||||||
@ -173,7 +172,7 @@ export class API {
|
|||||||
playlistId,
|
playlistId,
|
||||||
songIdToAdd: trackId,
|
songIdToAdd: trackId,
|
||||||
}
|
}
|
||||||
await this.get("rest/updatePlaylist", params);
|
await this.get('rest/updatePlaylist', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeFromPlaylist(playlistId: string, index: string) {
|
async removeFromPlaylist(playlistId: string, index: string) {
|
||||||
@ -181,20 +180,20 @@ export class API {
|
|||||||
playlistId,
|
playlistId,
|
||||||
songIndexToRemove: index,
|
songIndexToRemove: index,
|
||||||
}
|
}
|
||||||
await this.get("rest/updatePlaylist", params);
|
await this.get('rest/updatePlaylist', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRandomSongs() {
|
async getRandomSongs() {
|
||||||
const params = {
|
const params = {
|
||||||
size: 200,
|
size: 200,
|
||||||
};
|
}
|
||||||
const data = await this.get("rest/getRandomSongs", params);
|
const data = await this.get('rest/getRandomSongs', params)
|
||||||
return this.normalizeTrackList(data.randomSongs.song);
|
return this.normalizeTrackList(data.randomSongs.song)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStarred() {
|
async getStarred() {
|
||||||
return this
|
return this
|
||||||
.get("rest/getStarred2")
|
.get('rest/getStarred2')
|
||||||
.then(r => this.normalizeTrackList(r.starred2.song))
|
.then(r => this.normalizeTrackList(r.starred2.song))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,7 +203,7 @@ export class API {
|
|||||||
albumId: type === 'album' ? id : undefined,
|
albumId: type === 'album' ? id : undefined,
|
||||||
artistId: type === 'artist' ? 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) {
|
async unstar(type: 'track' | 'album' | 'artist', id: string) {
|
||||||
@ -213,27 +212,27 @@ export class API {
|
|||||||
albumId: type === 'album' ? id : undefined,
|
albumId: type === 'album' ? id : undefined,
|
||||||
artistId: type === 'artist' ? id : undefined,
|
artistId: type === 'artist' ? id : undefined,
|
||||||
}
|
}
|
||||||
await this.get("rest/unstar", params);
|
await this.get('rest/unstar', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query: string) {
|
async search(query: string) {
|
||||||
const params = {
|
const params = {
|
||||||
query,
|
query,
|
||||||
};
|
}
|
||||||
const data = await this.get("rest/search3", params);
|
const data = await this.get('rest/search3', params)
|
||||||
return {
|
return {
|
||||||
tracks: this.normalizeTrackList(data.searchResult3.song || []),
|
tracks: this.normalizeTrackList(data.searchResult3.song || []),
|
||||||
albums: (data.searchResult3.album || []).map((x: any) => this.normalizeAlbumResponse(x)),
|
albums: (data.searchResult3.album || []).map((x: any) => this.normalizeAlbumResponse(x)),
|
||||||
artists: (data.searchResult3.artist || []).map((x: any) => this.normalizeArtistResponse(x)),
|
artists: (data.searchResult3.artist || []).map((x: any) => this.normalizeArtistResponse(x)),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeTrackList(items: any[]) {
|
private normalizeTrackList(items: any[]) {
|
||||||
return items.map((item => ({
|
return items.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
url: this.getStreamUrl(item.id),
|
url: this.getStreamUrl(item.id),
|
||||||
image: this.getCoverArtUrl(item),
|
image: this.getCoverArtUrl(item),
|
||||||
})))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeAlbumResponse(item: any) {
|
private normalizeAlbumResponse(item: any) {
|
||||||
@ -252,14 +251,29 @@ export class API {
|
|||||||
|
|
||||||
private getCoverArtUrl(item: any) {
|
private getCoverArtUrl(item: any) {
|
||||||
if (!item.coverArt) {
|
if (!item.coverArt) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
const { server, username, salt, hash } = this.auth;
|
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`
|
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) {
|
private getStreamUrl(id: any) {
|
||||||
const { server, username, salt, hash } = this.auth;
|
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`
|
return `${server}/rest/stream` +
|
||||||
|
`?id=${id}` +
|
||||||
|
'&format=raw' +
|
||||||
|
'&v=1.15.0' +
|
||||||
|
`&u=${username}` +
|
||||||
|
`&s=${salt}` +
|
||||||
|
`&t=${hash}` +
|
||||||
|
`&c=${this.clientName}` +
|
||||||
|
'&size=300'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
BIconPersonCircle,
|
BIconPersonCircle,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
icon: { type: String }
|
icon: { type: String, required: true }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -42,11 +42,11 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: {
|
props: {
|
||||||
square: { type: Boolean, default: false },
|
square: { type: Boolean, default: false },
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
@ -7,7 +7,7 @@
|
|||||||
</b-dropdown>
|
</b-dropdown>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default Vue.extend({});
|
export default Vue.extend({})
|
||||||
</script>
|
</script>
|
@ -1,12 +1,12 @@
|
|||||||
<template functional>
|
<template functional>
|
||||||
<div>
|
<div>
|
||||||
<slot v-if="props.data" :data="props.data"></slot>
|
<slot v-if="props.data" :data="props.data" />
|
||||||
<div v-else class="text-center">
|
<div v-else class="text-center">
|
||||||
<span class="spinner-grow" />
|
<span class="spinner-grow" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
export default Vue.extend({})
|
export default Vue.extend({})
|
||||||
</script>
|
</script>
|
@ -1,6 +1,6 @@
|
|||||||
<template functional>
|
<template functional>
|
||||||
<div class="tiles" :class="props.square ? 'tiles-square' : 'tiles-rect'">
|
<div class="tiles" :class="props.square ? 'tiles-square' : 'tiles-rect'">
|
||||||
<slot></slot>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@ -29,11 +29,11 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
props: {
|
props: {
|
||||||
square: { type: Boolean, default: false },
|
square: { type: Boolean, default: false },
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
@ -1,10 +1,10 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue'
|
||||||
import ExternalLink from "./ExternalLink.vue";
|
import ExternalLink from './ExternalLink.vue'
|
||||||
import Icon from "./Icon.vue";
|
import Icon from './Icon.vue'
|
||||||
import OverflowMenu from "./OverflowMenu.vue";
|
import OverflowMenu from './OverflowMenu.vue'
|
||||||
import Spinner from "./Spinner.vue";
|
import Spinner from './Spinner.vue'
|
||||||
import Tiles from "./Tiles.vue";
|
import Tiles from './Tiles.vue'
|
||||||
import Tile from "./Tile.vue";
|
import Tile from './Tile.vue'
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
@ -13,11 +13,11 @@ const components = {
|
|||||||
Spinner,
|
Spinner,
|
||||||
Tiles,
|
Tiles,
|
||||||
Tile,
|
Tile,
|
||||||
};
|
}
|
||||||
|
|
||||||
type Key = keyof typeof components;
|
type Key = keyof typeof components;
|
||||||
|
|
||||||
Object.keys(components).forEach((_key) => {
|
Object.keys(components).forEach((_key) => {
|
||||||
const key = _key as keyof typeof components;
|
const key = _key as keyof typeof components
|
||||||
Vue.component(key, components[key]);
|
Vue.component(key, components[key])
|
||||||
});
|
})
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
Vue.filter('duration', (value: number) => {
|
||||||
Vue.filter("duration", (value: number) => {
|
const minutes = Math.floor(value / 60)
|
||||||
const minutes = Math.floor(value / 60);
|
const seconds = Math.floor(value % 60)
|
||||||
const seconds = Math.floor(value % 60);
|
return (minutes < 10 ? '0' : '') + minutes + ':' + (seconds < 10 ? '0' : '') + seconds
|
||||||
return (minutes < 10 ? '0' : '') + minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Vue.filter("dateTime", (value: string) => {
|
Vue.filter('dateTime', (value: string) => {
|
||||||
return value;
|
return value
|
||||||
})
|
})
|
@ -1,4 +1,3 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import Router from 'vue-router'
|
import Router from 'vue-router'
|
||||||
import Login from '@/auth/Login.vue'
|
import Login from '@/auth/Login.vue'
|
||||||
import Queue from '@/player/Queue.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 Playlist from '@/playlist/Playlist.vue'
|
||||||
import PlaylistList from '@/playlist/PlaylistList.vue'
|
import PlaylistList from '@/playlist/PlaylistList.vue'
|
||||||
import SearchResult from '@/search/SearchResult.vue'
|
import SearchResult from '@/search/SearchResult.vue'
|
||||||
import { AuthService } from '@/auth/service';
|
import { AuthService } from '@/auth/service'
|
||||||
|
|
||||||
|
|
||||||
export function setupRouter(auth: AuthService) {
|
export function setupRouter(auth: AuthService) {
|
||||||
const router = new Router({
|
const router = new Router({
|
||||||
@ -104,15 +102,15 @@ export function setupRouter(auth: AuthService) {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
});
|
})
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
if (to.name !== 'login' && !auth.isAuthenticated()) {
|
if (to.name !== 'login' && !auth.isAuthenticated()) {
|
||||||
next({name: 'login', query: { returnTo: to.fullPath }});
|
next({ name: 'login', query: { returnTo: to.fullPath } })
|
||||||
} else {
|
} else {
|
||||||
next();
|
next()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return router;
|
return router
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import Vuex, { Module } from 'vuex'
|
import Vuex, { Module } from 'vuex'
|
||||||
import { ActionContext } from "vuex"
|
|
||||||
import { playerModule } from "@/player/store"
|
import { playerModule } from '@/player/store'
|
||||||
import axios from 'axios';
|
import { AuthService } from '@/auth/service'
|
||||||
import { AuthService } from '@/auth/service';
|
import { API } from './api'
|
||||||
import { API } from './api';
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
@ -26,48 +24,48 @@ const setupRootModule = (authService: AuthService, api: API): Module<State, any>
|
|||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setError(state, error) {
|
setError(state, error) {
|
||||||
state.error = error;
|
state.error = error
|
||||||
},
|
},
|
||||||
clearError(state) {
|
clearError(state) {
|
||||||
state.error = null;
|
state.error = null
|
||||||
},
|
},
|
||||||
setLoginSuccess(state, { username, server }) {
|
setLoginSuccess(state, { username, server }) {
|
||||||
state.isLoggedIn = true;
|
state.isLoggedIn = true
|
||||||
state.username = username;
|
state.username = username
|
||||||
state.server = server;
|
state.server = server
|
||||||
},
|
},
|
||||||
toggleMenu(state) {
|
toggleMenu(state) {
|
||||||
state.showMenu = !state.showMenu;
|
state.showMenu = !state.showMenu
|
||||||
},
|
},
|
||||||
setPlaylists(state, playlists: any[]) {
|
setPlaylists(state, playlists: any[]) {
|
||||||
state.playlists = playlists
|
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) {
|
removePlaylist(state, id: string) {
|
||||||
state.playlists = state.playlists.filter(p => p.id !== id);
|
state.playlists = state.playlists.filter(p => p.id !== id)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
loadPlaylists({ commit }) {
|
loadPlaylists({ commit }) {
|
||||||
api.getPlaylists().then(result => {
|
api.getPlaylists().then(result => {
|
||||||
commit("setPlaylists", result);
|
commit('setPlaylists', result)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
createPlaylist({ commit }, name) {
|
createPlaylist({ commit }, name) {
|
||||||
api.createPlaylist(name).then(result => {
|
api.createPlaylist(name).then(result => {
|
||||||
commit("setPlaylists", result);
|
commit('setPlaylists', result)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addTrackToPlaylist({ }, { playlistId, trackId }) {
|
addTrackToPlaylist({ }, { playlistId, trackId }) {
|
||||||
api.addToPlaylist(playlistId, trackId);
|
api.addToPlaylist(playlistId, trackId)
|
||||||
},
|
},
|
||||||
deletePlaylist({ commit, state }, id) {
|
deletePlaylist({ commit }, id) {
|
||||||
api.deletePlaylist(id).then(() => {
|
api.deletePlaylist(id).then(() => {
|
||||||
commit("removePlaylist", id)
|
commit('removePlaylist', id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
export function setupStore(authService: AuthService, api: API) {
|
export function setupStore(authService: AuthService, api: API) {
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
@ -84,9 +82,9 @@ export function setupStore(authService: AuthService, api: API) {
|
|||||||
store.watch(
|
store.watch(
|
||||||
(state) => state.isLoggedIn,
|
(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 {
|
export function randomString(): string {
|
||||||
let arr = new Uint8Array(16);
|
let arr = new Uint8Array(16)
|
||||||
window.crypto.getRandomValues(arr);
|
window.crypto.getRandomValues(arr)
|
||||||
const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
arr = arr.map(x => validChars.charCodeAt(x % validChars.length));
|
arr = arr.map(x => validChars.charCodeAt(x % validChars.length))
|
||||||
return String.fromCharCode.apply(null, Array.from(arr));
|
return String.fromCharCode.apply(null, Array.from(arr))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function md5(str: string): string {
|
export function md5(str: string): string {
|
||||||
return MD5.hash(str);
|
return MD5.hash(str)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
devServer: {
|
devServer: {
|
||||||
disableHostCheck: true
|
disableHostCheck: true,
|
||||||
|
overlay: {
|
||||||
|
errors: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user