Compare commits

..

No commits in common. "master" and "v0.1.0" have entirely different histories.

8 changed files with 42 additions and 1033 deletions

View file

@ -1,36 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.3] - 2020-01-12
### Fixed
- Fixed volume actually not getting animated after first slider movement.
## [0.1.2] - 2020-01-12
### Added
- Slider styling
### Changed
- Slider now controls *maximum* volume independent of animated volume (i.e. acts as a volume fader)
## [0.1.1] - 2020-01-12
### Added
- Clear button for URLs.
- Basic layout responsiveness.
### Changed
- Move channel title to top.
## [0.1.0] - 2020-01-11
### Added
- First usable version - single animation mode yet; YouTube only.
[Unreleased]: https://gitlab.com/tmladek/noise-maker/compare/v0.1.3...master
[0.1.3]: https://gitlab.com/tmladek/noise-maker/compare/v0.1.2...v0.1.3
[0.1.2]: https://gitlab.com/tmladek/noise-maker/compare/v0.1.1...v0.1.2
[0.1.1]: https://gitlab.com/tmladek/noise-maker/compare/v0.1.0...v0.1.1
[0.1.0]: https://gitlab.com/tmladek/noise-maker/-/tags/v0.1.0

25
LICENSE
View file

@ -1,25 +0,0 @@
BSD 2-Clause License
Copyright (c) 2020, Tomáš Mládek
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

806
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "noisemaker", "name": "noisemaker",
"version": "0.1.3", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@ -14,12 +14,10 @@
"youtube-player": "^5.5.2" "youtube-player": "^5.5.2"
}, },
"devDependencies": { "devDependencies": {
"@types/youtube-player": "^5.5.1",
"@vue/cli-plugin-babel": "^4.1.0", "@vue/cli-plugin-babel": "^4.1.0",
"@vue/cli-plugin-typescript": "^4.1.0", "@vue/cli-plugin-typescript": "^4.1.0",
"@vue/cli-service": "^4.1.0", "@vue/cli-service": "^4.1.0",
"node-sass": "^4.13.0", "@types/youtube-player": "^5.5.1",
"sass-loader": "^8.0.1",
"typescript": "~3.5.3", "typescript": "~3.5.3",
"vue-template-compiler": "^2.6.10" "vue-template-compiler": "^2.6.10"
} }

View file

@ -18,7 +18,6 @@ import {PlayingState} from "@/common";
</label> </label>
<input :id="`url-input-${names[i-1]}`" type="text" v-model="urls[i - 1]"/> <input :id="`url-input-${names[i-1]}`" type="text" v-model="urls[i - 1]"/>
</div> </div>
<button class="channel-urls-clear-button" @click="urls = Array(N_CHANNELS).fill('')">CLEAR</button>
</div> </div>
</div> </div>
<div class="controls"> <div class="controls">
@ -47,7 +46,7 @@ export default class App extends Vue {
private playing: PlayingState = PlayingState.STOPPED; private playing: PlayingState = PlayingState.STOPPED;
private names = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu"]; private names = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu"];
private urls = Array(this.N_CHANNELS).fill(""); private urls = Array(this.N_CHANNELS).fill("");
private volumes = Array(this.N_CHANNELS).fill(100); private volumes = Array(this.N_CHANNELS).fill(50);
private states = Array(this.N_CHANNELS).fill(LoadingState.UNLOADED); private states = Array(this.N_CHANNELS).fill(LoadingState.UNLOADED);
private animateVolumeStart?: Date; private animateVolumeStart?: Date;
@ -134,14 +133,11 @@ h1 {
.channels-wrapper { .channels-wrapper {
display: flex; display: flex;
width: 80%; width: 80%;
flex-wrap: wrap;
} }
.channels { .channels {
display: flex; display: flex;
padding: 0 1rem 1rem 0; padding: 0 1rem;
justify-content: space-between;
flex-grow: 1;
} }
.channel { .channel {
@ -151,20 +147,14 @@ h1 {
.channel-urls { .channel-urls {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
justify-content: space-between; justify-content: space-between;
flex-grow: 1; width: 100%;
}
.channel-urls-clear-button {
max-width: 5em;
} }
.url-input { .url-input {
display: flex; display: flex;
width: 100%; width: 100%;
align-items: center; align-items: center;
margin: .2rem 0;
} }
.url-input label { .url-input label {

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

@ -1,9 +1,14 @@
<template> <template>
<div :class="['channel', `channel-${loadingState}`]"> <div :class="['channel', `channel-${loadingState}`]">
<div class="name">{{name}}</div> <div class="volume-wrapper">
<VolumeSlider :value="volume" :disabled="loadingState === 'unloaded'" <!--suppress HtmlFormInputWithoutLabel -->
@valueChange="(actualValue) => {actualVolume = actualValue}"/> <input class="volume" :disabled="loadingState === 'unloaded'"
<div class="title">{{loadingState === "loading" ? "Loading..." : title}}</div> type="range" min="0" max="100" v-model="volume"/>
</div>
<div class="description">
<div class="name">{{name}}</div>
<div class="title">{{loadingState === "loading" ? "Loading..." : title}}</div>
</div>
<div class="youtube-player" ref="ytpl"/> <div class="youtube-player" ref="ytpl"/>
</div> </div>
</template> </template>
@ -16,26 +21,22 @@ import YouTubePlayerFactory from "youtube-player";
import {YouTubePlayer} from "youtube-player/dist/types"; import {YouTubePlayer} from "youtube-player/dist/types";
import PlayerStates from "youtube-player/dist/constants/PlayerStates"; import PlayerStates from "youtube-player/dist/constants/PlayerStates";
import {LoadingState, PlayingState} from "@/common"; import {LoadingState, PlayingState} from "@/common";
import VolumeSlider from "@/components/VolumeSlider.vue";
enum SOURCE { enum SOURCE {
YouTube, YouTube,
DIRECT DIRECT
} }
@Component({ @Component
components: {VolumeSlider}
})
export default class Channel extends Vue { export default class Channel extends Vue {
@Prop() public name!: string; @Prop() public name: string = "Channel";
@Prop() public volume: number = 100; @Prop() public volume: number = 50;
@Prop() public url: string | undefined; @Prop() public url: string | undefined;
@Prop() public playing: PlayingState = PlayingState.STOPPED; @Prop() public playing: PlayingState = PlayingState.STOPPED;
private youtubePlayer?: YouTubePlayer; private youtubePlayer?: YouTubePlayer;
private loadingState = LoadingState.UNLOADED; private loadingState = LoadingState.UNLOADED;
private title = ""; private title = "";
private actualVolume = this.volume;
private get source() { private get source() {
if (this.url) { if (this.url) {
@ -50,7 +51,7 @@ export default class Channel extends Vue {
if (this.youtubePlayer) { if (this.youtubePlayer) {
switch (this.playing) { switch (this.playing) {
case PlayingState.PLAYING: case PlayingState.PLAYING:
this.youtubePlayer.setVolume(this.actualVolume); this.youtubePlayer.setVolume(this.volume);
this.youtubePlayer.playVideo(); this.youtubePlayer.playVideo();
break; break;
case PlayingState.STOPPED: case PlayingState.STOPPED:
@ -105,10 +106,10 @@ export default class Channel extends Vue {
} }
} }
@Watch("actualVolume") @Watch("volume")
private onActualVolumeChange() { private onVolumeChange() {
if (this.youtubePlayer) { if (this.youtubePlayer) {
this.youtubePlayer.setVolume(this.actualVolume); this.youtubePlayer.setVolume(this.volume);
} }
} }
@ -176,10 +177,27 @@ export default class Channel extends Vue {
background: lightgray; background: lightgray;
} }
.volume-wrapper {
--height: 180px;
--width: 30px;
height: var(--height);
width: var(--width);
display: block;
}
.volume {
position: relative;
width: var(--height);
height: var(--width);
top: calc(var(--height) / 2);
left: calc(var(--height) / 2 * -1 + var(--width) / 2);
transform: rotate(270deg);
}
.name { .name {
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
padding: 2px 0;
} }
.title { .title {

View file

@ -1,136 +0,0 @@
<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 = 80;
@Prop() public value!: number;
@Prop() public disabled!: boolean;
private mounted() {
this.onMaxValueChange();
}
@Watch("value")
@Watch("maxValue")
private onMaxValueChange() {
this.$emit("valueChange", 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);
min-width: var(--height);
height: var(--width);
min-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: -8px;
}
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>