add darkmode and progress bar
This commit is contained in:
		
							parent
							
								
									27ab4c870d
								
							
						
					
					
						commit
						4f7b922428
					
				
							
								
								
									
										16
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								src/App.vue
									
									
									
									
									
								
							@ -1,9 +1,13 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import DarkModeToggle from "@/components/DarkmodeToggle.vue";
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div v-if="$route.meta.allowEmbed && $route.query.embed === 'true'" class="p-2">
 | 
			
		||||
    <router-view />
 | 
			
		||||
  </div>
 | 
			
		||||
  <div v-else>
 | 
			
		||||
    <div class="bg-secondary shadow">
 | 
			
		||||
    <div class="bg-darkmode-dark bg-light shadow sticky-top">
 | 
			
		||||
      <nav class="navbar px-2 container">
 | 
			
		||||
        <router-link class="d-flex btn" to="/">
 | 
			
		||||
          <div class="d-flex header-title flex-column justify-content-center">
 | 
			
		||||
@ -11,14 +15,18 @@
 | 
			
		||||
            <div>music connects</div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </router-link>
 | 
			
		||||
        <div>
 | 
			
		||||
        <div class="d-flex align-items-center">
 | 
			
		||||
          <router-link
 | 
			
		||||
            v-if="!$api.isAuthorized()"
 | 
			
		||||
            to="/auth"
 | 
			
		||||
            class="btn btn-outline-dark"
 | 
			
		||||
            class="btn btn-outline"
 | 
			
		||||
          >
 | 
			
		||||
            login
 | 
			
		||||
          </router-link>
 | 
			
		||||
          <DarkModeToggle class="mx-2" v-slot="{ state }">
 | 
			
		||||
            <i v-if="state" class="bi-moon"></i>
 | 
			
		||||
            <i v-else class="bi-sun"></i>
 | 
			
		||||
          </DarkModeToggle>
 | 
			
		||||
        </div>
 | 
			
		||||
      </nav>
 | 
			
		||||
    </div>
 | 
			
		||||
@ -31,8 +39,6 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
@import "main.scss";
 | 
			
		||||
 | 
			
		||||
nav {
 | 
			
		||||
  a {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { defineProps } from "vue";
 | 
			
		||||
import TrackProgressBar from "@/components/TrackProgressBar.vue";
 | 
			
		||||
 | 
			
		||||
defineProps({
 | 
			
		||||
  currentlyPlaying: Object,
 | 
			
		||||
  currentlyPlaying: Object
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -11,46 +12,48 @@ defineProps({
 | 
			
		||||
    <div class="card-header">
 | 
			
		||||
      <b>{{ currentlyPlaying?.item.name }}</b>
 | 
			
		||||
      <div>
 | 
			
		||||
        {{ currentlyPlaying?.item.artists.map(artist => artist.name).join(', ') }}
 | 
			
		||||
        {{ currentlyPlaying?.item.artists.map(artist => artist.name).join(", ") }}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="card-body">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="col-md">
 | 
			
		||||
      <div class="row justify-content-center">
 | 
			
		||||
        <div class="col coverGroup">
 | 
			
		||||
          <img :src="currentlyPlaying.item.album.images[0].url" alt="album cover" class="card-img">
 | 
			
		||||
          <p v-if="currentlyPlaying.context">
 | 
			
		||||
            listening from {{ currentlyPlaying?.context.type }}
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md">
 | 
			
		||||
          <p class="my-2">
 | 
			
		||||
            <a
 | 
			
		||||
              :href="currentlyPlaying?.item.externalURL.spotify"
 | 
			
		||||
              target="_blank"
 | 
			
		||||
              rel="noopener norefferrer"
 | 
			
		||||
              class="btn btn-outline-dark m-1"
 | 
			
		||||
            >
 | 
			
		||||
              view track on Spotify
 | 
			
		||||
            </a>
 | 
			
		||||
            <a
 | 
			
		||||
              v-if="currentlyPlaying?.context"
 | 
			
		||||
              :href="currentlyPlaying?.context.externalURL.spotify"
 | 
			
		||||
              target="_blank"
 | 
			
		||||
              rel="noopener norefferrer"
 | 
			
		||||
              class="btn btn-outline-dark m-1"
 | 
			
		||||
            >
 | 
			
		||||
              view {{ currentlyPlaying?.context.type }} on Spotify
 | 
			
		||||
            </a>
 | 
			
		||||
          </p>
 | 
			
		||||
          <TrackProgressBar
 | 
			
		||||
            :duration="currentlyPlaying?.item.duration"
 | 
			
		||||
            :progress="currentlyPlaying.progress"
 | 
			
		||||
            :isPlaying="currentlyPlaying.isPlaying"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="card-footer">
 | 
			
		||||
      <div class="">
 | 
			
		||||
        <a
 | 
			
		||||
          :href="currentlyPlaying?.item.externalURL.spotify"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener norefferrer"
 | 
			
		||||
          class="btn btn-outline-dark m-1"
 | 
			
		||||
        >
 | 
			
		||||
          view track on Spotify
 | 
			
		||||
        </a>
 | 
			
		||||
        <a
 | 
			
		||||
          v-if="currentlyPlaying?.context"
 | 
			
		||||
          :href="currentlyPlaying?.context.externalURL.spotify"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener norefferrer"
 | 
			
		||||
          class="btn btn-outline-dark m-1"
 | 
			
		||||
        >
 | 
			
		||||
          view {{ currentlyPlaying?.context.type }} on Spotify
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.currentlyPlaying {
 | 
			
		||||
  .card-img {
 | 
			
		||||
  .coverGroup {
 | 
			
		||||
    max-width: 20rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { defineProps } from "vue";
 | 
			
		||||
import TrackProgressBar from "@/components/TrackProgressBar.vue";
 | 
			
		||||
 | 
			
		||||
defineProps({
 | 
			
		||||
  currentlyPlaying: Object,
 | 
			
		||||
@ -18,6 +19,11 @@ defineProps({
 | 
			
		||||
          <div>
 | 
			
		||||
            {{ currentlyPlaying?.item.artists.map(artist => artist.name).join(', ') }}
 | 
			
		||||
          </div>
 | 
			
		||||
          <TrackProgressBar
 | 
			
		||||
            :duration="currentlyPlaying?.item.duration"
 | 
			
		||||
            :progress="currentlyPlaying.progress"
 | 
			
		||||
            :isPlaying="currentlyPlaying.isPlaying"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										23
									
								
								src/components/DarkmodeToggle.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/components/DarkmodeToggle.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="form-check form-switch">
 | 
			
		||||
    <label class="custom-control-label" for="darkSwitch">
 | 
			
		||||
      <slot :state="sliderState" />
 | 
			
		||||
    </label>
 | 
			
		||||
    <input v-model="sliderState" type="checkbox" class="form-check-input" />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, watch } from 'vue';
 | 
			
		||||
import { themeConfig } from '@/main';
 | 
			
		||||
 | 
			
		||||
const sliderState = ref(themeConfig.getTheme() === 'dark');
 | 
			
		||||
 | 
			
		||||
watch(sliderState, (state) => {
 | 
			
		||||
  themeConfig.setTheme(state ? 'dark' : 'light');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
themeConfig.themeChangeHandlers.push((newTheme) => {
 | 
			
		||||
  sliderState.value = newTheme === 'dark';
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@ -52,14 +52,13 @@ const update = async (promise: Promise | unknown) => {
 | 
			
		||||
  loading.value = true;
 | 
			
		||||
  if (props.throbber) setTimeout(() => {
 | 
			
		||||
    if (loading.value) showThrobber.value = true;
 | 
			
		||||
  }, 250);
 | 
			
		||||
  }, 500);
 | 
			
		||||
  try {
 | 
			
		||||
    data.value = await (promise.isPromiseList
 | 
			
		||||
      ? Promise.all(promise.promises)
 | 
			
		||||
      : promise);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    error.value = e;
 | 
			
		||||
    console.error('PR', e);
 | 
			
		||||
  } finally {
 | 
			
		||||
    loading.value = false;
 | 
			
		||||
    showThrobber.value = false;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										30
									
								
								src/components/TimeFormatter.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/components/TimeFormatter.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { defineProps, watch, ref } from "vue";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  seconds: Number,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getHmsFromSeconds = (d: number) => ({
 | 
			
		||||
  hours: Math.floor(d / 3600),
 | 
			
		||||
  minutes: Math.floor(d % 3600 / 60),
 | 
			
		||||
  seconds: Math.floor(d % 3600 % 60),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getLeadingZero = (a: number, digits: number) =>
 | 
			
		||||
  ('0'.repeat(digits) + a.toString())
 | 
			
		||||
    .substr(- digits);
 | 
			
		||||
 | 
			
		||||
const hms = ref(getHmsFromSeconds(props.seconds as number));
 | 
			
		||||
 | 
			
		||||
watch(() => props.seconds, (to) => {
 | 
			
		||||
  hms.value = getHmsFromSeconds(to as number);
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <span v-if="hms.hours" class="hours">{{ getLeadingZero(hms.hours, 2) }}:</span>
 | 
			
		||||
  <span class="minutes">{{ getLeadingZero(hms.minutes, 2) }}:</span>
 | 
			
		||||
  <span class="seconds">{{ getLeadingZero(hms.seconds, 2) }}</span>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										52
									
								
								src/components/TrackProgressBar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/components/TrackProgressBar.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { defineProps, ref, watch, onBeforeUnmount } from "vue";
 | 
			
		||||
import TimeFormatter from "@/components/TimeFormatter.vue";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  duration: Number,
 | 
			
		||||
  progress: Number,
 | 
			
		||||
  isPlaying: Boolean,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const estimatedProgress = ref(props.progress as number);
 | 
			
		||||
 | 
			
		||||
const updateInterval = setInterval(() => {
 | 
			
		||||
  if (props.isPlaying) estimatedProgress.value += 1000;
 | 
			
		||||
}, 1000);
 | 
			
		||||
 | 
			
		||||
watch(() => props.progress, (to) => {
 | 
			
		||||
  estimatedProgress.value = to as number;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
  clearInterval(updateInterval);
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <div class="progress my-2">
 | 
			
		||||
      <div
 | 
			
		||||
        class="progress-bar"
 | 
			
		||||
        role="progressbar"
 | 
			
		||||
        :style="{
 | 
			
		||||
          width: `${ estimatedProgress / duration * 100 }%`
 | 
			
		||||
        }"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="row my-2">
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <TimeFormatter :seconds="estimatedProgress / 1000"/>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col-auto">
 | 
			
		||||
        <TimeFormatter :seconds="duration / 1000"/>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.progress {
 | 
			
		||||
  height: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@ -1,8 +1,7 @@
 | 
			
		||||
@import "bootstrap/scss/bootstrap.scss";
 | 
			
		||||
//@import "bootstrap-darkmode/scss/darktheme.scss";
 | 
			
		||||
@import "../node_modules/bootstrap/scss/bootstrap.scss";
 | 
			
		||||
@import "../node_modules/bootstrap-darkmode/scss/darktheme.scss";
 | 
			
		||||
 | 
			
		||||
html, body {
 | 
			
		||||
  //background-color: $dark-body-bg;
 | 
			
		||||
  line-break: loose;
 | 
			
		||||
  word-break: break-word;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,12 @@ import App from "./App.vue";
 | 
			
		||||
import "./registerServiceWorker";
 | 
			
		||||
import router from "./router";
 | 
			
		||||
import { createApi } from "@/Api";
 | 
			
		||||
import 'bootstrap-icons/font/bootstrap-icons.scss';
 | 
			
		||||
import './main.scss';
 | 
			
		||||
import { ThemeConfig } from 'bootstrap-darkmode';
 | 
			
		||||
 | 
			
		||||
export const themeConfig = new ThemeConfig();
 | 
			
		||||
themeConfig.initTheme();
 | 
			
		||||
 | 
			
		||||
createApp(App)
 | 
			
		||||
  .use(router)
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ const userInfo = ref(api?.getUserInfo(route.params.id as string));
 | 
			
		||||
 | 
			
		||||
let refreshUserInfo = setInterval(() => {
 | 
			
		||||
  userInfo.value = api?.getUserInfo(route.params.id as string);
 | 
			
		||||
}, 20000);
 | 
			
		||||
}, 10000);
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
  clearInterval(refreshUserInfo);
 | 
			
		||||
@ -43,10 +43,7 @@ onBeforeUnmount(() => {
 | 
			
		||||
      v-slot="{ data: { user, currentlyPlaying } }"
 | 
			
		||||
      class="row"
 | 
			
		||||
    >
 | 
			
		||||
      <div v-if="$route.query.embed === 'true'">
 | 
			
		||||
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-else class="col-md-4">
 | 
			
		||||
      <div class="col-md-4">
 | 
			
		||||
        <div class="card">
 | 
			
		||||
          <div class="card-header">
 | 
			
		||||
            {{ user.displayName }}
 | 
			
		||||
@ -80,6 +77,9 @@ onBeforeUnmount(() => {
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <h2>Currently listening to:</h2>
 | 
			
		||||
        <CurrentlyPlaying v-if="currentlyPlaying?.item" :currently-playing="currentlyPlaying" />
 | 
			
		||||
        <p v-else class="alert alert-info">
 | 
			
		||||
          {{ user.displayName }} is not listening to music.
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </PromiseResolver>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user