slider: now controls maximum volume; styling; its own component
also added SCSS
This commit is contained in:
parent
2c7afa6273
commit
3983620d81
5 changed files with 953 additions and 35 deletions
806
package-lock.json
generated
806
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -14,10 +14,12 @@
|
|||
"youtube-player": "^5.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/youtube-player": "^5.5.1",
|
||||
"@vue/cli-plugin-babel": "^4.1.0",
|
||||
"@vue/cli-plugin-typescript": "^4.1.0",
|
||||
"@vue/cli-service": "^4.1.0",
|
||||
"@types/youtube-player": "^5.5.1",
|
||||
"node-sass": "^4.13.0",
|
||||
"sass-loader": "^8.0.1",
|
||||
"typescript": "~3.5.3",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class App extends Vue {
|
|||
private playing: PlayingState = PlayingState.STOPPED;
|
||||
private names = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu"];
|
||||
private urls = Array(this.N_CHANNELS).fill("");
|
||||
private volumes = Array(this.N_CHANNELS).fill(50);
|
||||
private volumes = Array(this.N_CHANNELS).fill(100);
|
||||
private states = Array(this.N_CHANNELS).fill(LoadingState.UNLOADED);
|
||||
|
||||
private animateVolumeStart?: Date;
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
<template>
|
||||
<div :class="['channel', `channel-${loadingState}`]">
|
||||
<div class="name">{{name}}</div>
|
||||
<div class="volume-wrapper">
|
||||
<!--suppress HtmlFormInputWithoutLabel -->
|
||||
<input class="volume" :disabled="loadingState === 'unloaded'"
|
||||
type="range" min="0" max="100" v-model="volume"/>
|
||||
</div>
|
||||
<VolumeSlider :value="volume" :disabled="loadingState === 'unloaded'"
|
||||
@maxValueChange="(actualValue) => {actualVolume = actualValue}"/>
|
||||
<div class="title">{{loadingState === "loading" ? "Loading..." : title}}</div>
|
||||
<div class="youtube-player" ref="ytpl"/>
|
||||
</div>
|
||||
|
@ -19,22 +16,26 @@ import YouTubePlayerFactory from "youtube-player";
|
|||
import {YouTubePlayer} from "youtube-player/dist/types";
|
||||
import PlayerStates from "youtube-player/dist/constants/PlayerStates";
|
||||
import {LoadingState, PlayingState} from "@/common";
|
||||
import VolumeSlider from "@/components/VolumeSlider.vue";
|
||||
|
||||
enum SOURCE {
|
||||
YouTube,
|
||||
DIRECT
|
||||
}
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
components: {VolumeSlider}
|
||||
})
|
||||
export default class Channel extends Vue {
|
||||
@Prop() public name: string = "Channel";
|
||||
@Prop() public volume: number = 50;
|
||||
@Prop() public name!: string;
|
||||
@Prop() public volume: number = 100;
|
||||
@Prop() public url: string | undefined;
|
||||
@Prop() public playing: PlayingState = PlayingState.STOPPED;
|
||||
|
||||
private youtubePlayer?: YouTubePlayer;
|
||||
private loadingState = LoadingState.UNLOADED;
|
||||
private title = "";
|
||||
private actualVolume = this.volume;
|
||||
|
||||
private get source() {
|
||||
if (this.url) {
|
||||
|
@ -49,7 +50,7 @@ export default class Channel extends Vue {
|
|||
if (this.youtubePlayer) {
|
||||
switch (this.playing) {
|
||||
case PlayingState.PLAYING:
|
||||
this.youtubePlayer.setVolume(this.volume);
|
||||
this.youtubePlayer.setVolume(this.actualVolume);
|
||||
this.youtubePlayer.playVideo();
|
||||
break;
|
||||
case PlayingState.STOPPED:
|
||||
|
@ -104,10 +105,10 @@ export default class Channel extends Vue {
|
|||
}
|
||||
}
|
||||
|
||||
@Watch("volume")
|
||||
private onVolumeChange() {
|
||||
@Watch("actualVolume")
|
||||
private onActualVolumeChange() {
|
||||
if (this.youtubePlayer) {
|
||||
this.youtubePlayer.setVolume(this.volume);
|
||||
this.youtubePlayer.setVolume(this.actualVolume);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,24 +176,6 @@ export default class Channel extends Vue {
|
|||
background: lightgray;
|
||||
}
|
||||
|
||||
.volume-wrapper {
|
||||
--height: 180px;
|
||||
--width: 30px;
|
||||
height: var(--height);
|
||||
width: var(--width);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.volume {
|
||||
position: relative;
|
||||
width: var(--height);
|
||||
height: var(--width);
|
||||
left: calc(var(--height) / 2 * -1 + var(--width) / 2);
|
||||
transform: rotate(270deg);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
|
|
133
src/components/VolumeSlider.vue
Normal file
133
src/components/VolumeSlider.vue
Normal file
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<div class="volume-wrapper">
|
||||
<!--suppress HtmlFormInputWithoutLabel -->
|
||||
<input class="volume" :disabled="disabled"
|
||||
type="range" min="0" max="100" v-model="maxValue"
|
||||
:style="`--value: ${value}%; --max-value: ${maxValue}%; --actual-value: ${value * (maxValue / 100)}%`"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class Channel extends Vue {
|
||||
private name = "VolumeSlider";
|
||||
|
||||
private maxValue = 50;
|
||||
@Prop() public value!: number;
|
||||
@Prop() public disabled!: boolean;
|
||||
|
||||
private mounted() {
|
||||
this.onMaxValueChange();
|
||||
}
|
||||
|
||||
@Watch("maxValue")
|
||||
private onMaxValueChange() {
|
||||
this.$emit("maxValueChange", this.value * (this.maxValue / 100), this.maxValue);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.volume-wrapper {
|
||||
--height: 180px;
|
||||
--width: 30px;
|
||||
height: var(--height);
|
||||
width: var(--width);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.volume {
|
||||
position: relative;
|
||||
width: var(--height);
|
||||
height: var(--width);
|
||||
left: calc(var(--height) / 2 * -1 + var(--width) / 2);
|
||||
transform: rotate(270deg);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin range-track() {
|
||||
height: 10px;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(90deg,
|
||||
black 0,
|
||||
black var(--actual-value), white var(--actual-value),
|
||||
white var(--max-value), transparent var(--max-value), transparent 100%);
|
||||
border: 1px solid #010101;
|
||||
}
|
||||
|
||||
|
||||
@mixin range-thumb {
|
||||
border: 1px solid #000000;
|
||||
height: 24px;
|
||||
width: 12px;
|
||||
background: #ffffff;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
@include range-track;
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-track {
|
||||
@include range-track;
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
@include range-thumb;
|
||||
-webkit-appearance: none;
|
||||
margin-top: -14px;
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-thumb {
|
||||
@include range-thumb;
|
||||
}
|
||||
|
||||
/*
|
||||
input[type=range]::-ms-track {
|
||||
width: 100%;
|
||||
height: 8.4px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
border-width: 16px 0;
|
||||
color: transparent;
|
||||
}
|
||||
input[type=range]::-ms-fill-lower {
|
||||
background: #2a6495;
|
||||
border: 0.2px solid #010101;
|
||||
border-radius: 2.6px;
|
||||
}
|
||||
input[type=range]::-ms-fill-upper {
|
||||
background: #3071a9;
|
||||
border: 0.2px solid #010101;
|
||||
border-radius: 2.6px;
|
||||
}
|
||||
input[type=range]::-ms-thumb {
|
||||
border: 1px solid #000000;
|
||||
height: 36px;
|
||||
width: 16px;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type=range]:focus::-ms-fill-lower {
|
||||
background: #3071a9;
|
||||
}
|
||||
input[type=range]:focus::-ms-fill-upper {
|
||||
background: #367ebd;
|
||||
}*/
|
||||
|
||||
</style>
|
Loading…
Reference in a new issue