migration: remote controller

This commit is contained in:
Phan An 2022-04-25 16:07:38 +03:00
parent 43f4547871
commit f0d14d4ed3
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
10 changed files with 163 additions and 201 deletions

View file

@ -1,6 +1,6 @@
import './staticLoader'
import { createApp } from 'vue'
import App from './app.vue'
import App from './App.vue'
import { httpService } from '@/services'
import { clickaway, droppable, focus } from '@/directives'
import router from '@/router'
@ -22,6 +22,4 @@ app.directive('koel-droppable', droppable)
*/
app.mount('#app')
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').then((): void => console.log('Service Worker Registered'))
}
navigator.serviceWorker.register('./sw.js').then((): void => console.log('Service Worker Registered'))

View file

@ -6,7 +6,7 @@
For those that don't need to maintain their own UI state, we use v-if and enjoy some code-splitting juice.
-->
<Visualizer v-if="showingVisualizer"/>
<AlbumArtOverlay :song="currentSong" v-if="preferences.showAlbumArtOverlay"/>
<AlbumArtOverlay v-if="showAlbumArtOverlay && currentSong" :album="currentSong?.album"/>
<HomeScreen v-show="view === 'Home'"/>
<QueueScreen v-show="view === 'Queue'"/>
@ -25,12 +25,12 @@
<SettingsScreen v-if="view === 'Settings'"/>
<ProfileScreen v-if="view === 'Profile'"/>
<UserListScreen v-if="view === 'Users'"/>
<YoutubeScreen v-if="sharedState.useYouTube" v-show="view === 'YouTube'"/>
<YoutubeScreen v-if="useYouTube" v-show="view === 'YouTube'"/>
</section>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, reactive, ref } from 'vue'
import { defineAsyncComponent, reactive, ref, toRef } from 'vue'
import { eventBus } from '@/utils'
import { preferenceStore, commonStore } from '@/stores'
import HomeScreen from '@/components/screens/HomeScreen.vue'
@ -54,8 +54,8 @@ const SearchExcerptsScreen = defineAsyncComponent(() => import('@/components/scr
const SearchSongResultsScreen = defineAsyncComponent(() => import('@/components/screens/search/SearchSongResultsScreen.vue'))
const Visualizer = defineAsyncComponent(() => import('@/components/ui/Visualizer.vue'))
const preferences = reactive(preferenceStore.state)
const sharedState = reactive(commonStore.state)
const showAlbumArtOverlay = toRef(preferenceStore.state, 'showAlbumArtOverlay')
const useYouTube = toRef(commonStore.state, 'useYouTube')
const showingVisualizer = ref(false)
const screenProps = ref<any>(null)
const view = ref<MainViewName>('Home')

View file

@ -6,18 +6,16 @@
import { ref, toRefs, watchEffect } from 'vue'
import { albumStore } from '@/stores'
const props = defineProps<{ song: Song | null }>()
const { song } = toRefs(props)
const props = defineProps<{ album: Album }>()
const { album } = toRefs(props)
const thumbnailUrl = ref<String | null>(null)
watchEffect(async () => {
if (song.value) {
try {
thumbnailUrl.value = await albumStore.getThumbnail(song.value.album)
} catch (e) {
thumbnailUrl.value = null
}
try {
thumbnailUrl.value = await albumStore.getThumbnail(album.value)
} catch (e) {
thumbnailUrl.value = null
}
})
</script>

View file

@ -1,12 +1,12 @@
<template>
<div id="app" :class="{ 'standalone' : inStandaloneMode }">
<div id="wrapper" :class="{ 'standalone' : inStandaloneMode }">
<template v-if="authenticated">
<album-art-overlay :album="album" v-if="preferences.showAlbumArtOverlay"/>
<AlbumArtOverlay v-if="showAlbumArtOverlay && album" :album="album"/>
<main>
<template v-if="connected">
<div class="details" v-if="song">
<div class="cover" :style="{ backgroundImage: 'url('+song.album.cover+')' }"/>
<div :style="{ backgroundImage: `url(${song.album.cover})` }" class="cover"/>
<div class="info">
<div class="wrap">
<p class="title text">{{ song.title }}</p>
@ -18,7 +18,7 @@
<p class="none text-secondary" v-else>No song is playing.</p>
<footer>
<a class="favorite" @click.prevent="toggleFavorite">
<i class="fa fa-heart yep" v-if="song && song.liked"></i>
<i class="fa fa-heart yep" v-if="song?.liked"></i>
<i class="fa fa-heart-o" v-else></i>
</a>
<a class="prev" @click="playPrev">
@ -32,7 +32,12 @@
<i class="fa fa-step-forward"></i>
</a>
<span class="volume">
<span id="volumeSlider" v-show="showingVolumeSlider" v-koel-clickaway="closeVolumeSlider"/>
<span
v-show="showingVolumeSlider"
ref="volumeSlider"
id="volumeSlider"
v-koel-clickaway="closeVolumeSlider"
/>
<span class="icon" @click.stop="toggleVolumeSlider">
<i class="fa fa-volume-off" v-if="muted"/>
<i class="fa fa-volume-up" v-else/>
@ -47,7 +52,7 @@
</div>
<p v-else>
No active Koel instance found.
<a @click.prevent="rescan" class="rescan text-orange">Rescan</a>
<a class="rescan text-orange" @click.prevent="rescan">Rescan</a>
</p>
</div>
</main>
@ -59,188 +64,139 @@
</div>
</template>
<script lang="ts">
import Vue from 'vue'
<script lang="ts" setup>
import noUISlider from 'nouislider'
import { socketService, authService } from '@/services'
import { userStore, preferenceStore } from '@/stores'
import LoginForm from '@/components/auth/LoginForm.vue'
import { authService, socketService } from '@/services'
import { preferenceStore, userStore } from '@/stores'
import { SliderElement } from 'koel/types/ui'
import { clickaway } from '@/directives'
import { computed, defineAsyncComponent, nextTick, onMounted, ref, toRef, watch } from 'vue'
import LoginForm from '@/components/auth/LoginForm.vue'
let volumeSlider: SliderElement
const MAX_RETRIES = 10
const DEFAULT_VOLUME = 7
export default Vue.extend({
components: {
LoginForm,
AlbumArtOverlay: () => import('@/components/ui/AlbumArtOverlay.vue')
},
const AlbumArtOverlay = defineAsyncComponent(() => import('@/components/ui/AlbumArtOverlay.vue'))
directives: {
'koel-clickaway': clickaway
},
const volumeSlider = ref<SliderElement>()
const authenticated = ref(false)
const song = ref<Song | null>(null)
const connected = ref(false)
const muted = ref(false)
const showingVolumeSlider = ref(false)
const retries = ref(0)
const showAlbumArtOverlay = toRef(preferenceStore.state, 'showAlbumArtOverlay')
const volume = ref(DEFAULT_VOLUME)
data: () => ({
authenticated: false,
song: null as unknown as Song,
lastActiveTime: new Date().getTime(),
inStandaloneMode: false,
connected: false,
muted: false,
showingVolumeSlider: false,
retries: 0,
preferences: preferenceStore.state,
volume: DEFAULT_VOLUME
}),
const inStandaloneMode = ref(
(window.navigator as any).standalone || window.matchMedia('(display-mode: standalone)').matches
)
watch: {
async connected (): Promise<void> {
await this.$nextTick()
watch(connected, async () => {
await nextTick()
volumeSlider = document.getElementById('volumeSlider') as SliderElement
if (!volumeSlider.value) return
noUISlider.create(volumeSlider, {
orientation: 'vertical',
connect: [true, false],
start: this.volume || DEFAULT_VOLUME,
range: { min: 0, max: 10 },
direction: 'rtl'
noUISlider.create(volumeSlider.value, {
orientation: 'vertical',
connect: [true, false],
start: volume.value || DEFAULT_VOLUME,
range: { min: 0, max: 10 },
direction: 'rtl'
})
if (!volumeSlider.value.noUiSlider) {
throw new Error('Failed to initialize noUISlider on element #volumeSlider')
}
volumeSlider.value.noUiSlider.on('change', (values: number[], handle: number): void => {
const volume = values[handle]
muted.value = !volume
socketService.broadcast('SOCKET_SET_VOLUME', { volume })
})
})
watch(volume, () => volumeSlider.value?.noUiSlider!.set(volume.value || DEFAULT_VOLUME))
const onUserLoggedIn = () => {
authenticated.value = true
init()
}
const init = async () => {
try {
const user = await userStore.getProfile()
userStore.init([], user)
await socketService.init()
socketService
.listen('SOCKET_SONG', ({ song: _song }: { song: Song }) => (song.value = _song))
.listen('SOCKET_PLAYBACK_STOPPED', () => song.value && (song.value.playbackState = 'Stopped'))
.listen('SOCKET_VOLUME_CHANGED', (volume: number) => volumeSlider.value?.noUiSlider?.set(volume))
.listen('SOCKET_STATUS', ({ song: _song, volume: _volume }: { song: Song, volume: number }) => {
song.value = _song
volume.value = _volume || DEFAULT_VOLUME
connected.value = true
})
if (!volumeSlider.noUiSlider) {
throw new Error('Failed to initialize noUISlider on element #volumeSlider')
}
scan()
} catch (e) {
console.error(e)
authenticated.value = false
}
}
volumeSlider.noUiSlider.on('change', (values: number[], handle: number): void => {
const volume = values[handle]
this.muted = !volume
socketService.broadcast('SOCKET_SET_VOLUME', { volume })
})
},
const toggleVolumeSlider = () => (showingVolumeSlider.value = !showingVolumeSlider.value)
const closeVolumeSlider = () => (showingVolumeSlider.value = false)
volume: (value: number): void => {
if (!volumeSlider) {
return
}
const toggleFavorite = () => {
if (!song.value) {
return
}
volumeSlider.noUiSlider!.set(value || DEFAULT_VOLUME)
song.value.liked = !song.value.liked
socketService.broadcast('SOCKET_TOGGLE_FAVORITE')
}
const togglePlayback = () => {
if (song.value) {
song.value.playbackState = song.value.playbackState === 'Playing' ? 'Paused' : 'Playing'
}
socketService.broadcast('SOCKET_TOGGLE_PLAYBACK')
}
const playNext = () => socketService.broadcast('SOCKET_PLAY_NEXT')
const playPrev = () => socketService.broadcast('SOCKET_PLAY_PREV')
const getStatus = () => socketService.broadcast('SOCKET_GET_STATUS')
const scan = () => {
if (!connected.value) {
if (!maxRetriesReached.value) {
getStatus()
retries.value++
window.setTimeout(scan, 1000)
}
},
} else {
retries.value = 0
}
}
methods: {
onUserLoggedIn (): void {
this.authenticated = true
this.init()
},
const rescan = () => {
retries.value = 0
scan()
}
async init (): Promise<void> {
try {
const user = await userStore.getProfile()
userStore.init([], user)
const playing = computed(() => Boolean(song.value?.playbackState === 'Playing'))
const maxRetriesReached = computed(() => retries.value >= MAX_RETRIES)
const album = computed(() => song.value?.album)
await socketService.init()
socketService
.listen('SOCKET_SONG', ({ song }: { song: Song }): void => {
this.song = song
})
.listen('SOCKET_PLAYBACK_STOPPED', (): void => {
this.song && (this.song.playbackState = 'Stopped')
})
.listen('SOCKET_VOLUME_CHANGED', (volume: number): void => volumeSlider.noUiSlider!.set(volume))
.listen('SOCKET_STATUS', ({ song, volume }: { song: Song, volume: number }): void => {
this.song = song
this.volume = volume || DEFAULT_VOLUME
this.connected = true
})
this.scan()
} catch (e) {
this.authenticated = false
}
},
toggleVolumeSlider (): void {
this.showingVolumeSlider = !this.showingVolumeSlider
},
closeVolumeSlider (): void {
this.showingVolumeSlider = false
},
toggleFavorite (): void {
if (!this.song) {
return
}
this.song.liked = !this.song.liked
socketService.broadcast('SOCKET_TOGGLE_FAVORITE')
},
togglePlayback (): void {
if (this.song) {
this.song.playbackState = this.song.playbackState === 'Playing' ? 'Paused' : 'Playing'
}
socketService.broadcast('SOCKET_TOGGLE_PLAYBACK')
},
playNext: (): void => {
socketService.broadcast('SOCKET_PLAY_NEXT')
},
playPrev: (): void => {
socketService.broadcast('SOCKET_PLAY_PREV')
},
getStatus: (): void => {
socketService.broadcast('SOCKET_GET_STATUS')
},
scan (): void {
if (!this.connected) {
if (!this.maxRetriesReached) {
this.getStatus()
this.retries++
window.setTimeout(this.scan, 1000)
}
} else {
this.retries = 0
}
},
rescan (): void {
this.retries = 0
this.scan()
}
},
computed: {
playing (): boolean {
return Boolean(this.song && this.song.playbackState === 'Playing')
},
maxRetriesReached (): boolean {
return this.retries >= MAX_RETRIES
},
album (): Album | null {
return this.song ? this.song.album : null
}
},
created (): void {
this.inStandaloneMode = (window.navigator as any).standalone
},
mounted (): void {
// The app has just been initialized, check if we can get the user data with an already existing token
if (authService.hasToken()) {
this.authenticated = true
this.init()
}
onMounted(() => {
// The app has just been initialized, check if we can get the user data with an already existing token
if (authService.hasToken()) {
authenticated.value = true
init()
}
})
</script>
@ -252,7 +208,7 @@ body, html {
height: 100vh;
}
#app {
#wrapper {
height: 100vh;
background: var(--color-bg-primary);
@ -315,7 +271,7 @@ body, html {
}
main {
height: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
@ -404,13 +360,14 @@ main {
}
.play-pause {
display: inline-block;
width: 16vmin;
height: 16vmin;
border: 1px solid var(--color-text-primary);
border-radius: 50%;
line-height: 16vmin;
font-size: 7vmin;
display: flex;
place-content: center;
place-items: center;
&.fa-play {
margin-left: 4px;
@ -419,10 +376,10 @@ main {
}
}
#app.standalone {
#wrapper.standalone {
padding-top: 20px;
#main {
main {
.details {
.cover {
width: calc(80vw - 4px);
@ -474,7 +431,7 @@ main {
height: 16px;
border-radius: 50%;
border: 0;
left: -4px;
left: -12px;
top: 0;
background: var(--color-highlight);
box-shadow: none;

View file

@ -1,7 +1,12 @@
import './staticLoader'
import { httpService } from '@/services'
import App from './app.vue'
import { createApp } from 'vue'
import { httpService } from '@/services'
import App from './App.vue'
import { clickaway } from '@/directives'
httpService.init()
createApp(App).mount('#app')
const app = createApp(App)
app.directive('koel-clickaway', clickaway)
app.mount('#app')

View file

@ -15,10 +15,12 @@ interface BroadcastSongData {
liked: boolean
playbackState: PlaybackState
album: {
id: number
name: string
cover: string
}
artist: {
id: number
name: string
}
}
@ -226,10 +228,12 @@ export const songStore = {
liked: song.liked,
playbackState: song.playbackState || 'Stopped',
album: {
id: song.album.id,
name: song.album.name,
cover: song.album.cover
},
artist: {
id: song.artist.id,
name: song.artist.name
}
}

View file

@ -3,7 +3,7 @@
We actually should use a library like dotenv-webpack to load .env variables.
Unfortunately, can't get it to work for now :(
--}}
window.BASE_URL = "{{ asset('') }}";
window.PUSHER_APP_KEY = "{{ config('broadcasting.connections.pusher.key') }}";
window.PUSHER_APP_CLUSTER = "{{ config('broadcasting.connections.pusher.options.cluster') }}";
window.BASE_URL = @json(asset(''));
window.PUSHER_APP_KEY = @json(config('broadcasting.connections.pusher.key'));
window.PUSHER_APP_CLUSTER = @json(config('broadcasting.connections.pusher.options.cluster'));
</script>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>Koel - Remote Controller</title>

View file

@ -41,8 +41,8 @@ mix.copy('resources/assets/img', 'public/img')
mix.ts('resources/assets/js/app.ts', 'public/js').vue({ version: 3 })
.sass('resources/assets/sass/app.scss', 'public/css')
// .ts('resources/assets/js/remote/app.ts', 'public/js/remote').vue({ version: 3 })
// .sass('resources/assets/sass/remote.scss', 'public/css')
.ts('resources/assets/js/remote/app.ts', 'public/js/remote').vue({ version: 3 })
.sass('resources/assets/sass/remote.scss', 'public/css')
if (mix.inProduction()) {
mix.version()