add global player

This commit is contained in:
Stepan Fedyanin 2024-06-11 17:49:36 +05:00
parent b9be44bdde
commit dd80e653ae
16 changed files with 233 additions and 129 deletions

View File

@ -1,15 +1,15 @@
<template> <template>
<div class="app"> <div class="app">
<AppHeader :showAuthentication="showAuthentication" @shopAuthentication="changeShowAuthentication"/> <AppHeader/>
<div class="app__block"> <div class="app__block">
<Page404 v-if="showErrorPage404"/> <Page404 v-if="showErrorPage404"/>
<Page500 v-if="showErrorPage500"/> <Page500 v-if="showErrorPage500"/>
<routerView v-else/> <routerView v-else/>
</div> </div>
<Player @shopAuthentication="changeShowAuthentication"/> <Player/>
<AppFooter/> <AppFooter/>
</div> </div>
<Authentication :showModal="showAuthentication" @hideModal="changeShowAuthentication"/> <Authentication/>
</template> </template>
<script> <script>
@ -33,7 +33,6 @@ export default {
}, },
data() { data() {
return { return {
showAuthentication: false,
showSidebarBlock: false, showSidebarBlock: false,
}; };
}, },
@ -56,9 +55,6 @@ export default {
mounted() { mounted() {
}, },
methods: { methods: {
changeShowAuthentication(value) {
this.showAuthentication = value;
}
} }
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
.footer{ .footer{
padding: 0 0 var(--space-between-block); padding-bottom: var(--space-between-block);
width: 100%; width: 100%;
&__top{ &__top{
padding-bottom: 80px; padding-bottom: 80px;

View File

@ -29,6 +29,10 @@
&__content{ &__content{
max-width: var(--container); max-width: var(--container);
margin: 0 auto; margin: 0 auto;
padding: 0 50px;
@mixin responsive-l {
padding: 0 20px;
}
.m--indentation{ .m--indentation{
} }
} }

View File

@ -51,7 +51,12 @@
&__subtitle { &__subtitle {
text-align: center; text-align: center;
max-width: 600px;
margin: 0 auto;
@mixin responsive-l {
padding: 0 20px;
}
span { span {
font-weight: 700; font-weight: 700;
} }
@ -105,26 +110,31 @@
max-height: var(--base-content-size); max-height: var(--base-content-size);
background-size: contain; background-size: contain;
scale: 0.8; scale: 0.8;
margin-left: calc(-1920px + 100vw); margin-left: calc((100vw + -1 * 1920px) / 2);
} }
&__description { &__description {
margin-top: calc(var(--space-between-sections) / 2); margin-top: calc(var(--space-between-sections) / 2);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
margin-bottom: var(--space-between-sections); margin-bottom: var(--space-between-sections);
@mixin responsive-m {
gap: var(--space-between-block);
}
} }
&__info { &__info {
width: 60%; width: 60%;
@mixin responsive-m {
width: 100%;
}
&--item { &--item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
color: transparent; color: transparent;
background-clip: text; background-clip: text;
background-image: var(--linear-gradient); background-image: var(--linear-gradient);
&.m--circle { &.m--circle {
align-items: end; align-items: end;
@ -141,20 +151,32 @@
font-weight: 400; font-weight: 400;
color: var(--color-white); color: var(--color-white);
width: 57%; width: 57%;
@mixin responsive-m {
width: 75%;
}
} }
} }
} }
&__content { &__content {
max-width: 360px; max-width: 360px;
@mixin responsive-m {
display: flex;
align-items: flex-start;
gap: calc(var(--space-between-block) / 2);
max-width: none;
}
&--item { &--item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
font-size: 16px; font-size: 16px;
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
position: relative;
z-index: 2;
@mixin responsive-m {
width: calc(100% / 3);
}
&:before { &:before {
content: ''; content: '';
height: 50px; height: 50px;

View File

@ -8,6 +8,8 @@
background-image: var(--linear-gradient); background-image: var(--linear-gradient);
margin: 0.75rem 0; margin: 0.75rem 0;
gap: 1rem; gap: 1rem;
position: relative;
z-index: 2;
.spinner { .spinner {
width: 48px; width: 48px;
height: 48px; height: 48px;

View File

@ -5,7 +5,7 @@
gap: 0 var(--space-between-block); gap: 0 var(--space-between-block);
} }
&-item{ &-item{
width: calc(50% - 40px / 2); width: calc(50% - var(--space-between-block) / 2);
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -5,12 +5,12 @@
<img src="@/assets/img/icon/logo.svg" alt="logo"/> <img src="@/assets/img/icon/logo.svg" alt="logo"/>
</div> </div>
<ul class="header__menu" :class="showMenu&&'m--active'"> <ul class="header__menu" :class="showMenu&&'m--active'">
<div class="header__burger m--active m--menu" @click="handlerShowMenu"> <li class="header__burger m--active m--menu" @click="handlerShowMenu">
<span></span> <span></span>
</div> </li>
<div class="header__logo m--menu" @click="next('home')"> <li class="header__logo m--menu" @click="next('home')">
<img src="@/assets/img/icon/logo.svg" alt="logo"/> <img src="@/assets/img/icon/logo.svg" alt="logo"/>
</div> </li>
<li <li
v-for="(item, key) in menu" v-for="(item, key) in menu"
:key="key" :key="key"
@ -38,14 +38,7 @@
<script> <script>
export default { export default {
name: 'app-header', name: 'app-header',
props:{ props:{},
showAuthentication:{
type: Boolean,
default(){
return false
}
}
},
data(){ data(){
return{ return{
menuList:[ menuList:[
@ -117,12 +110,15 @@ export default {
if (item.role==='login' && !this.user?.id) return !this.user?.id if (item.role==='login' && !this.user?.id) return !this.user?.id
return false return false
}) })
} },
showAuthentication(){
return this.$store.state.showAuthModal
},
}, },
methods:{ methods:{
handlerClick(methods){ handlerClick(methods){
if (methods==='login'){ if (methods==='login'){
this.$emit('shopAuthentication', true) this.$store.dispatch('setShowAuthModal', true);
} }
if (methods==='profile'){ if (methods==='profile'){
this.next('profile') this.next('profile')

View File

@ -6,11 +6,11 @@
content-transition="vfm-fade" content-transition="vfm-fade"
overlay-transition="vfm-fade" overlay-transition="vfm-fade"
:clickToClose="false" :clickToClose="false"
@click-outside="$emit('hideModal')" @click-outside="close()"
> >
<button <button
class="button modal__close" class="button modal__close"
@click="$emit('hideModal')" @click="close"
> >
</button> </button>
<div class="authentication"> <div class="authentication">
@ -40,7 +40,7 @@
outerClass: '$reset', outerClass: '$reset',
}" }"
@submit="submitHandler" @submit="submitHandler"
@click-outside="$emit('hideModal')" @click-outside="close"
> >
<FormKitSchema :schema="loginForm"/> <FormKitSchema :schema="loginForm"/>
</FormKit> </FormKit>
@ -73,14 +73,6 @@ import {app} from "@/services";
export default { export default {
name: 'authentication', name: 'authentication',
props: {
showModal: {
type: Boolean,
default() {
return false;
}
},
},
data() { data() {
return { return {
currentTabsItem: 'login', currentTabsItem: 'login',
@ -144,9 +136,9 @@ export default {
} }
}, },
computed: { computed: {
show() { show(){
return this.showModal; return this.$store.state.showAuthModal
}, }
}, },
methods: { methods: {
changeTab(tab) { changeTab(tab) {
@ -158,8 +150,8 @@ export default {
this.$store.dispatch('setToken', res); this.$store.dispatch('setToken', res);
app.user().then(user=>{ app.user().then(user=>{
this.$store.dispatch('setUser', user); this.$store.dispatch('setUser', user);
this.close();
this.next('profile'); this.next('profile');
this.$emit('hideModal');
}).catch(err=>{ }).catch(err=>{
console.log(err) console.log(err)
}) })
@ -171,8 +163,8 @@ export default {
this.$store.dispatch('setToken', res); this.$store.dispatch('setToken', res);
app.user().then(user=>{ app.user().then(user=>{
this.$store.dispatch('setUser', user); this.$store.dispatch('setUser', user);
this.close();
this.next('profile'); this.next('profile');
this.$emit('hideModal');
}).catch(err=>{ }).catch(err=>{
console.log(err) console.log(err)
}) })
@ -183,7 +175,7 @@ export default {
}, },
close() { close() {
this.$emit('hidden', false); this.$store.dispatch('setShowAuthModal', false);
}, },
next(name){ next(name){
this.$router.push({name}); this.$router.push({name});

View File

@ -5,7 +5,7 @@
</div> </div>
<div class="player__content"> <div class="player__content">
<div class="player__top"> <div class="player__top">
<button v-if="isPlay" @click="handlerPause" class="button player__btn m--pause"> <button v-if="currentPlay.isPlay" @click="handlerPause" class="button player__btn m--pause">
</button> </button>
<button v-else @click="handlerPlay" class="button player__btn m--play"> <button v-else @click="handlerPlay" class="button player__btn m--play">
</button> </button>
@ -16,8 +16,9 @@
<div class="player__favorites" :class="[isFavorites&&'m--active']" @click="handlerFavorites"> <div class="player__favorites" :class="[isFavorites&&'m--active']" @click="handlerFavorites">
</div> </div>
<div class="player__tools"> <div class="player__tools">
{{isLive}}
<FormKit <FormKit
v-model="isPlayRadio" v-model="isLive"
type="toggle" type="toggle"
label="Включить мою музыку" label="Включить мою музыку"
/> />
@ -40,46 +41,48 @@
</template> </template>
<script> <script>
import {audio as Player} from "@/services"; import {audio as Player, app} from "@/services";
import {app} from "@/services";
export default { export default {
name: 'player', name: 'player',
data() { data() {
return { return {
audioUrl: 'http://82.97.242.49:18000/radio.mp3', audioUrl: 'http://82.97.242.49:18000/radio.mp3',
isPlay: false,
isFavorites: false, isFavorites: false,
isPlayRadio: true, isPlayRadio: true,
connection: null, connection: null,
currentPlay: {}, isLive: false
player: {
target: null,
volume: 50,
progress: 100,
live: false,
}
} }
},
created() {
this.connectionPlayer();
},
mounted() {
this.initPlayer();
// this.$refs.player.addEventListener('timeupdate', this.updateProgress);
}, },
computed:{ computed:{
user() { user() {
return this.$store.state.user; return this.$store.state.user;
}, },
}, currentPlay(){
methods: { return this.$store.state.currentPlay
initPlayer(){
this.player.target = document.createElement('audio')
this.player.target.src = '';
this.player.target.preload = 'auto';
this.player.target.controls = true
}, },
player(){
return this.$store.state.player
}
},
watch:{
'currentPlay': {
immediate: true,
handler() {
console.debug('this.currentPlay.live',!this.currentPlay.live)
this.isLive = !this.currentPlay.live
},
}
},
created() {
this.connectionPlayer();
},
mounted() {
// if (!this.player.target){
this.$store.dispatch('initPlayer');
// }
},
methods: {
connectionPlayer() { connectionPlayer() {
if (this.connection) { if (this.connection) {
this.connection.removePlay(); this.connection.removePlay();
@ -92,11 +95,12 @@ export default {
const jsonData = JSON.parse(e.data) const jsonData = JSON.parse(e.data)
if (jsonData?.pub?.data){ if (jsonData?.pub?.data){
const data = jsonData?.pub?.data; const data = jsonData?.pub?.data;
if (data.np.station.listen_url!==this.player.target.src){ if (this.currentPlay.live){
this.player.target.src = data.np.station.listen_url; if (data.np.station.listen_url!==this.player.target.src){
this.player.live = true; this.$store.dispatch('changePlayer', data.np.station.listen_url);
}
this.$store.dispatch('setCurrentPlay', {...data.np.now_playing.song, live: false});
} }
this.currentPlay = data.np.now_playing.song;
} }
}, },
updateProgress(){ updateProgress(){
@ -104,17 +108,10 @@ export default {
console.log(this.$refs.player.duration) console.log(this.$refs.player.duration)
}, },
handlerPlay() { handlerPlay() {
if (this.player.target?.play){ this.$store.dispatch('handlerPlayer', {play: true});
this.player.target.play();
this.isPlay = true;
}
}, },
handlerPause() { handlerPause() {
console.log(this.player.target?.pause) this.$store.dispatch('handlerPlayer', {pause: true});
if (this.player.target.src) {
this.player.target.pause();
this.isPlay = false;
}
}, },
handlerFavorites(){ handlerFavorites(){
if (this.user?.id){ if (this.user?.id){
@ -129,11 +126,10 @@ export default {
}, },
setVolume(){ setVolume(){
this.player.volume = this.player.volume === 0? 50: 0; this.$store.dispatch('handlerPlayer', {volume: 100});
this.player.target.volume = this.player.volume / 100; },
},
changeVolume() { changeVolume() {
this.player.target.volume = this.player.volume / 100; this.$store.dispatch('handlerPlayer', {volume: 100});
}, },
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="song-item" :class="[selectSong===song&&'m--select']" @click="handlerSelectSong"> <div class="song-item" :class="[currentPlay.id===song.id&&'m--select']" @click="handlerSelectSong">
<div class="song-item__selected" :class="[!isPlay&&'m--stop']"> <div class="song-item__selected" :class="[!isPlay&&'m--stop']">
<svg width="33" height="28" viewBox="0 0 33 28" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="33" height="28" viewBox="0 0 33 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="" d="M0 15C0 13.6193 1.11929 12.5 2.5 12.5C3.88071 12.5 5 13.6193 5 15V27.5H0V15Z" fill="#7138F4"/> <path class="" d="M0 15C0 13.6193 1.11929 12.5 2.5 12.5C3.88071 12.5 5 13.6193 5 15V27.5H0V15Z" fill="#7138F4"/>
@ -16,8 +16,8 @@
</button> </button>
</div> </div>
<div class="song-item__info"> <div class="song-item__info">
<span>Название трека</span> <span>{{song.title}}</span>
Исполнитель {{song.artist}}
</div> </div>
<div class="song-item__tools"> <div class="song-item__tools">
<button class="button song-item__btn m--small m--favorites"></button> <button class="button song-item__btn m--small m--favorites"></button>
@ -37,9 +37,9 @@ export default {
} }
}, },
song:{ song:{
type: Number, type: Object,
default(){ default(){
return null return {}
} }
}, },
playSong:{ playSong:{
@ -62,6 +62,11 @@ export default {
isPlay: this.playSong isPlay: this.playSong
} }
}, },
computed:{
currentPlay(){
return this.$store.state.currentPlay
}
},
methods:{ methods:{
handlerPlay() { handlerPlay() {
this.isPlay = true; this.isPlay = true;
@ -70,7 +75,7 @@ export default {
this.isPlay = false; this.isPlay = false;
}, },
handlerSelectSong(){ handlerSelectSong(){
this.$emit('changeSelect', this.song) this.$store.dispatch('setCurrentPlay', {...this.song, live: false});
} }
} }
} }

View File

@ -1,12 +1,9 @@
<template> <template>
<div class="song-list"> <div class="song-list">
<SongItem <SongItem
v-for="i in 10" v-for="song in songList"
:key="i" :key="song"
:song="i" :song="song"
:playSong="i===selectSong"
:selectSong="selectSong"
@changeSelect="changeSelectSong"
/> />
</div> </div>
</template> </template>
@ -17,15 +14,15 @@ import SongItem from "@/components/song-item.vue";
export default { export default {
name: 'song-list', name: 'song-list',
components: {SongItem}, components: {SongItem},
data(){ props: {
return{ songList: {
selectSong: null type: Array,
default: () => []
} }
}, },
methods:{ data() {
changeSelectSong(id){ return {}
this.selectSong = id; },
} methods: {}
}
} }
</script> </script>

View File

@ -27,6 +27,34 @@ export default class extends REST {
}); });
} }
static getTeams(station, params) {
return this._get(`radio/teams`, params, {}).then((data) => {
return data;
}).catch((error) => {
throw new RESTError(error, 'Ошибка при получении команды');
});
}
static createFavoriteForUser(params){
return this._post(`radio/song/add_favorite`, {}, params).then((data) => {
return data;
}).catch((error) => {
throw new RESTError(error, 'Ошибка при получении плейлистов');
});
}
static getFavoriteList(params){
return this._get(`radio/song`, {}, params).then((data) => {
return data;
}).catch((error) => {
throw new RESTError(error, 'Ошибка при получении плейлистов');
});
}
static getNews(station, params) { static getNews(station, params) {
return this._get(`radio.mp3`, params, {}).then((data) => { return this._get(`radio.mp3`, params, {}).then((data) => {
return data; return data;
@ -41,13 +69,7 @@ export default class extends REST {
throw new RESTError(error, 'Ошибка при получении плейлистов'); throw new RESTError(error, 'Ошибка при получении плейлистов');
}); });
} }
static getTeams(station, params) {
return this._get(`radio/teams`, params, {}).then((data) => {
return data;
}).catch((error) => {
throw new RESTError(error, 'Ошибка при получении команды');
});
}
static getRubriks(station, params) { static getRubriks(station, params) {
return this._get(`radio.mp3`, params, {}).then((data) => { return this._get(`radio.mp3`, params, {}).then((data) => {
return data; return data;
@ -55,11 +77,4 @@ export default class extends REST {
throw new RESTError(error, 'Ошибка при получении плейлистов'); throw new RESTError(error, 'Ошибка при получении плейлистов');
}); });
} }
static createFavoriteForUser(params){
return this._post(`radio/song/create_song`, {}, params).then((data) => {
return data;
}).catch((error) => {
throw new RESTError(error, 'Ошибка при получении плейлистов');
});
}
} }

View File

@ -20,7 +20,7 @@ class RESTError extends Error {
//if (this.response && (this.response.status === 401 || this.response.status === 403)) { //if (this.response && (this.response.status === 401 || this.response.status === 403)) {
if (this.response && (this.response.status === 401)) { if (this.response && (this.response.status === 401)) {
store.dispatch('deathUser'); store.dispatch('deathUser');
router.push({ name: 'auth' }); store.dispatch('setShowAuthModal', true);
} else { } else {
if (typeof Error.captureStackTrace === 'function') { if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);

View File

@ -2,7 +2,7 @@ import { createStore } from 'vuex'
import VuexPersist from 'vuex-persist'; import VuexPersist from 'vuex-persist';
const vuexPersist = new VuexPersist({ const vuexPersist = new VuexPersist({
key: 'tugan-v-cirk-ne-hodim', key: 'it-radio',
//storage: window.localStorage //storage: window.localStorage
}); });
@ -14,8 +14,15 @@ export default createStore({
token: null, token: null,
refreshToken: null, refreshToken: null,
user: null, user: null,
showAuthModal: false,
station: { station: {
id: 1 id: 1
},
currentPlay: {},
player:{
target: null,
volume: 50,
live: false,
} }
} }
}, },
@ -32,6 +39,38 @@ export default createStore({
state.token = null; state.token = null;
state.refreshToken = null; state.refreshToken = null;
}, },
setCurrentPlay(state, song){
state.currentPlay = song;
},
setShowAuthModal(state, show){
state.showAuthModal = show
},
setPlayer(state, params){
state.player = {...state.player,...params}
},
initPlayer(state){
state.player.target = document.createElement('audio')
state.player.target.src = '';
state.player.target.preload = 'auto';
state.player.target.controls = true;
console.log(state.player.target)
},
changePlayer(state, params){
state.player.target.src = params;
},
handlerPlayer(state, params){
if (params.pause){
state.currentPlay.isPlay = false;
state.player.target.pause();
}
if (params.play){
state.currentPlay.isPlay = true;
state.player.target.play();
}
if (params.volume){
state.player.target.volume = params.volume;
}
},
}, },
actions: { actions: {
setToken(context, tokens) { setToken(context, tokens) {
@ -44,5 +83,24 @@ export default createStore({
context.commit('user', {}); context.commit('user', {});
context.commit('removeToken'); context.commit('removeToken');
}, },
setCurrentPlay(context, song){
context.commit('setCurrentPlay', song);
},
setShowAuthModal(context, show){
context.commit('setShowAuthModal', show);
},
setPlayer(context, params){
context.commit('setPlayer', params);
},
initPlayer(context){
context.commit('initPlayer');
},
handlerPlayer(context, params){
context.commit('handlerPlayer', params);
},
changePlayer(context, params){
context.commit('changePlayer', params);
}
} }
}); });

View File

@ -3,9 +3,9 @@
<div class="home__meaning" ref="targetWrapper"> <div class="home__meaning" ref="targetWrapper">
<div class="app__content"> <div class="app__content">
<p class="text home__subtitle"> <p class="text home__subtitle">
Мы цифровое <span>онлайн радио.</span> Мы цифровое <span>онлайн радио.</span><br/>
Помогаем разобраться в том, что такое <span>IT.</span> Помогаем разобраться в том, что такое <span>IT.</span><br/>
Находимся в <span>Челябинске</span>, но вещаем на весь <span>Мир</span> Находимся в <span>Челябинске</span>, но вещаем на весь <span>Мир</span><br/>
</p> </p>
<h1 class="home__title" ref="targetTitle"> <h1 class="home__title" ref="targetTitle">
IT-Радио <span id="targetTitleSpan">радиостанция про сферу</span> технологий <br/> и развитие <span id="targetTitleSpan">в IT</span> IT-Радио <span id="targetTitleSpan">радиостанция про сферу</span> технологий <br/> и развитие <span id="targetTitleSpan">в IT</span>
@ -19,7 +19,7 @@
<div class="home__banner" ref="target"> <div class="home__banner" ref="target">
</div> </div>
</div> </div>
<template v-if="false"> <template v-if="true">
<div class="app__content"> <div class="app__content">
<div class="home__description"> <div class="home__description">

View File

@ -20,7 +20,12 @@
</button> </button>
</div> </div>
<template v-if="currentTabsItem==='music'"> <template v-if="currentTabsItem==='music'">
<SongList/> <template v-if="showLoader">
<div class="loader">
<div class="spinner" /> Загрузка данных
</div>
</template>
<SongList v-else :songList="songList"/>
</template> </template>
</div> </div>
</template> </template>
@ -28,6 +33,7 @@
<script> <script>
import AppBreadcrumbs from "@/components/app-breadcrumbs.vue"; import AppBreadcrumbs from "@/components/app-breadcrumbs.vue";
import SongList from "@/components/song-list.vue"; import SongList from "@/components/song-list.vue";
import {app} from "@/services";
export default { export default {
name: 'profile', name: 'profile',
@ -49,6 +55,8 @@ export default {
name: 'playlists' name: 'playlists'
}, },
], ],
songList:[],
showLoader:true,
} }
}, },
watch: { watch: {
@ -63,7 +71,20 @@ export default {
}, },
} }
}, },
created() {
this.getSongList();
},
methods: { methods: {
getSongList(){
this.showLoader = true;
app.getFavoriteList().then(res=>{
this.showLoader = false;
this.songList = res;
}).catch(err=>{
this.showLoader = false;
console.error(err)
})
},
changeTab(tab) { changeTab(tab) {
this.currentTabsItem = tab; this.currentTabsItem = tab;
this.$router.push({name: this.$route.name, hash: `#${tab}`}); this.$router.push({name: this.$route.name, hash: `#${tab}`});