Vue- watch for audio time change - vue.js

I have an audio file which plays songs. I am currently making a slider that is equal to the songs length and current time. You can't use player.currentTime in the watch component in Vue so how would you go about updating a value in realtime equal to player current time.
I currently have the v-model="player.currentTime" but that only updates when I pause the songs and not real-time.
This is what I have so far
player: new Audio()
this.player.src = this.songs[this.songIndex].src
this.player.play()
Player:
<input
type="range"
name="timeStamp"
ref="time"
v-model.lazy="this.player.currentTime"
step="0.1"
class="w-[100%] hover:cursor-pointer"
/>

You have to listen to the timeupdate event. I made a simple sample code:
Output:
<template>
<div style="border: 1px solid gray; border-radius: 5px; padding: 5px;">
<div>
<button #click="play">Play | Pause</button>
{{ timeLabel }}
</div>
<div>
<input
type="range"
:min="0"
:max="duration"
v-model="currentTime"
#input="updateTime"
>
</div>
</div>
</template>
<script>
export default {
name: 'BaseAudioPlayerTest',
data() {
return {
src: 'Spring-Flowers.mp3',
player: null,
duration: 0,
currentTime: 0,
timeLabel: '00:00:00',
};
},
methods: {
play() {
if (this.player.paused) {
this.player.play();
this.duration = this.player.duration;
} else {
this.player.pause();
}
},
updateTime() {
this.player.currentTime = this.currentTime;
},
timeupdate() {
this.currentTime = this.player.currentTime;
const hr = Math.floor(this.currentTime / 3600);
const min = Math.floor((this.currentTime - (hr * 3600)) / 60);
const sec = Math.floor(this.currentTime - (hr * 3600) - (min * 60));
this.timeLabel = `${hr.toString()
.padStart(2, '0')}:${min.toString()
.padStart(2, '0')}:${sec.toString()
.padStart(2, '0')}`;
},
},
mounted() {
this.player = new Audio(this.src);
this.player.addEventListener('timeupdate', this.timeupdate, false);
},
};
</script>
You can find more info here.

Related

Multiple range inputs related on number from parent

I have 4 range inputs. Each of them has min number 0, max number 10.
In Total they can't sum to more than 22.
One way to approach this would be to disable all inputs once they hit 22 and add a reset button. I would find it to be more user-friendly to allow the ranges to be decremented after the max is reached instead of a whole reset.
I tried disabling if it's less or equal 0, but the scroller was still under control.
Check the comments on the
sandbox here if it easier , but the parent class is as below:
<template>
<div class="vote">
<div class="vote__title">Left: <span>{{ hmLeft }}</span> votes</div>
<div class="vote__body">
<div v-for="user in activeInnerPoll" :key="user._id">
<userVoteFor :hmLeft="hmLeft" #cntCount="cntCount" :id="user._id"/>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex"
import userVoteFor from "#/components/userVoteFor";
export default {
name: "Vote.vue",
components: {
userVoteFor
},
data(){
return {
votes: 22,
objRes: {} // that's where we write what id of a user and how many counts
}
},
computed: {
...mapGetters("polls", ["activeInnerPoll"]), // array of objects {_id : "some_id", cnt: 0}
hmLeft(){ // how much left, counter which tells how many votes left
let sum = 0;
for(let key in this.objRes){
sum += this.objRes[key];
}
return this.votes - sum;
}
},
methods: {
cntCount(id, cnt){ // emit for children, gets id and cnt of input-range and sets to result obj
this.objRes[id] = parseInt(cnt);
}
}
}
</script>
<style scoped lang="scss">
#import "#/assets/vars.scss";
#import "#/assets/base.scss";
.vote{
&__title{
#include center;
margin-top: 15px;
span{
font-size: 20px;
margin: 0 5px;
color: $pink;
}
}
}
</style>
Child class here:
<template>
<div class="vote__component">
<label class="vote__component__label" :for="id">{{ playerNameById( id )}}</label>
<input #input="check($event)" // thought maybe something to do with event ?
:disabled="disable"
class="vote__component__input"
:id="id"
type="range"
min="0"
max="10"
step="1"
v-model="cnt">
<div class="vote__component__res">{{ cnt }}</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "userVoteFor.vue",
props: {
id: {
type: String,
required: true
},
hmLeft: {
type: Number,
required: true
}
},
emits: ["cntCount"],
data() {
return {
cnt: 0,
disable: false,
lastVal: 0
}
},
computed: {
...mapGetters("user", ["playerNameById"]) // gets map object which stores names for user by id
},
methods: {
check(e){
console.log(e);
if(this.hmLeft <= 0) { //HERE IS APART WHERE I THINK SHOULD BE WRITTEN LOGIC if hmLeft <= 0 then ... , else write cnt in resObj and computed var will calc how many votes left
this.lastVal = this.cnt;
this.cnt = this.lastVal;
}
else this.$emit("cntCount", this.id, this.cnt);
}
}
}
</script>
<style scoped lang="scss">
.vote__component{
width: 80%;
margin: 10px auto;
position: relative;
display: flex;
justify-content: right;
padding: 10px 0;
font-size: 15px;
&__input{
margin-left: auto;
width: 60%;
margin-right: 20px;
}
&__res{
position: absolute;
top: 20%;
right: 0;
}
&__label{
}
}
</style>
The way I'd implement this is by using a watch and the get and set method of computed.
The array of values would be updated via a computed. This makes it easy to hook into a v-model and allows us to maintain reactivity with the original array.
The watch is then used to compute the total that is available. Then, for bonus points, we can use the total to adjust the width of the input so the step size remains consistent.
Even though this is using the composition Api, you can implement that using data, watch and computed the classical way
const makeRange = (max, vals, index) => {
const defaultMax = 10;
const num = Vue.computed({
get: () => vals[index],
set: value => vals[index] = Number(value)
});
const total = Vue.computed(() => vals.reduce((a, b) => a + b, 0), vals);
const style = Vue.computed(() => {
return `width: ${(numMax.value * 12 + 20)}px`
})
const numMax = Vue.computed(() => {
return Math.min(defaultMax, (num.value + max - total.value))
}, total);
return {num, numMax, style};
};
const app = Vue.createApp({
setup() {
const vals = Vue.reactive([5, 5, 5])
const max = 22;
const ranges = vals.map((v,i)=>makeRange(max, vals, i));
// helpers for visualising
const total = Vue.computed(() => vals.reduce((a, b) => a + b, 0), vals);
const totalLeft = Vue.computed(() => max - total.value , total.value);
return {ranges, vals, totalLeft, total, max};
}
}).mount('#app');
<script src="https://unpkg.com/vue#3.0.2/dist/vue.global.prod.js"></script>
<div id="app">
<li v-for="range in ranges">
<input
:style="range.style.value"
type="range" min="0"
:max="range.numMax.value"
v-model="range.num.value"
>
value: {{range.num.value}}
max: {{range.numMax.value}}
</li>
<li>{{ vals.join(' + ') }} = {{ total }}</li>
<li>max is {{ max }} , minus total {{total }} is {{ totalLeft }}</li>
</div>

canvas size is different when drawing canvas size

Could you tell me how to fix this issue: canvas has 1200x700px, but the drawing is scaled to 300x150?
<template>
<div>
<input type="text" v-model="msg"></input>
<br>
<canvas v-on:mousemove="mouse" id="c"></canvas>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
msg: 'Welcome to Your Vue.js App',
vueCanvas: null,
pixel: null
}
},
methods: {
mouse: function (event) {
this.vueCanvas.putImageData(this.pixel, event.offsetX, event.offsetY)
this.msg = event.offsetX + ":" + event.offsetY
},
init: function () {
this.vueCanvas = document.getElementById("c").getContext("2d");
this.pixel = this.vueCanvas.createImageData(1, 1);
this.pixel.data[3] = 255;
}
},
mounted() {
this.init()
}
}
</script>
<style scoped>
#c {
height: 700px;
width: 1200px;
border: 1px solid gray;
}
</style>
Snippet:
new Vue({
el: "#app",
data() {
return {
msg: 'Welcome to Your Vue.js App',
vueCanvas: null,
pixel: null,
};
},
methods: {
mouse: function(event) {
this.vueCanvas.putImageData(this.pixel, event.offsetX, event.offsetY);
this.msg = event.offsetX + ':' + event.offsetY;
},
init: function() {
this.vueCanvas = document.getElementById('c').getContext('2d');
this.pixel = this.vueCanvas.createImageData(1, 1);
this.pixel.data[3] = 255;
},
},
mounted() {
this.init();
},
})
#c {
height: 700px;
width: 1200px;
border: 1px solid gray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model="msg" />
<br />
<canvas v-on:mousemove="mouse" id="c"></canvas>
</div>
I have recorded a video in order to show what goes wrong.
I find solution:
this.vueCanvas.canvas.width=this.vueCanvas.canvas.clientWidth //300->1200
this.vueCanvas.canvas.height=this.vueCanvas.canvas.clientHeight //150->700
but I am not sure if is it good practice.
This is how I meant in my comment:
new Vue({
el: "#app",
data() {
return {
msg: 'Welcome to Your Vue.js App',
vueCanvas: null,
pixel: null,
};
},
methods: {
mouse: function(event) {
this.vueCanvas.putImageData(this.pixel, event.offsetX, event.offsetY);
this.msg = event.offsetX + ':' + event.offsetY;
},
init: function() {
this.vueCanvas = document.getElementById('c').getContext('2d');
this.pixel = this.vueCanvas.createImageData(1, 1);
this.pixel.data[3] = 255;
},
},
mounted() {
this.init();
},
})
#c {
border: 1px solid gray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model="msg" />
<br />
<canvas v-on:mousemove="mouse" id="c" width="1200" height="700"></canvas>
</div>
Is this not how you want it?

How to make the props binding reactive when using 'createElement' function

Vue.config.devtools = false
Vue.config.productionTip = false
let modal = Vue.extend({
template: `
<div class="modal">
<p>Balance: {{ balance }}</p>
<input #input="$emit('input', $event.target.value)" :value="value">
<button #click="$emit('input', balance)">ALL</button>
</div>
`,
props: ['balance', 'value']
})
function makeComponent(data) {
return { render(h) { return h(modal, data) } }
}
Vue.component('app', {
template: `
<div>
<p>Balance: {{ balance }}</p>
<p>To withdraw: {{ withdrawAmount }}</p>
<p>Will remain: {{ balance - withdrawAmount }}</p>
<button #click="onClick">Withdraw</button>
<modal-container ref="container"/>
</div>`,
data () {
return {
withdrawAmount: 0,
balance: 123
}
},
methods: {
onClick () {
this.$refs.container.show(makeComponent({
props: {
balance: String(this.balance),
value: String(this.withdrawAmount)
},
on: {
input: (value) => {
this.withdrawAmount = Number(value)
}
}
}))
}
}
})
Vue.component('modal-container', {
template: `
<div>
<component v-if="canShow" :is="modal"/>
</div>
`,
data () {
return { modal: undefined, canShow: false }
},
methods: {
show (modal) {
this.modal = modal
this.canShow = true
},
hide () {
this.canShow = false
this.modal = undefined
}
}
})
new Vue({
el: '#app'
})
.modal {
background-color: gray;
width: 300px;
height: 100px;
margin: 10px;
padding: 10px;
}
* {
font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
color: #2c3e50;
line-height: 25px;
font-size: 14px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17-beta.0/vue.js"></script>
<div id="app">
<app></app>
</div>
This is a simplified version of our application using vue-js-modal. The basic Idea is that we pass a component declaration along with required VNodeData to the plugin function and it would append that component to a pre-defined DOM point with proper data bindings.
And our usecase is that:
User clicks 'Withdraw'
Modal pops up with an input field and static field displaying user's balance
User enters the amount he/she wants to withdraw and that amount and resulting balance are displayed in the callee component.
There is a 'ALL' button besides the input field for user to easily enter a number equal to his/her balance
Problem: When user clicks 'ALL', the 'modal' component fires an 'input' event with value of the balance and the callee component receives that event and updates its 'withdrawAmount'. But the 'withdrawAmount' is supposed to be passed back to the 'modal' as 'value' prop and further updates the input field, which doesn't happen.

Vue JS animation/transition effect on specific v-for item text on function call

I would like to create a transition/animation effect from a method where the text changes upon firing an event (not created) customTextAnim(key) which works independently for each of the v-for items.
When run, the text appears larger (22px), then shrinks to the normal 14px size after about a .3 second animation.
The text I would like to animate starts out 1t 14px, then jumps to 22px and shrinks back down to 14px. This is the text i would like to animate this.auctions[key].username*
I have literally no idea how to do this, i really need all the help i can get
<template>
<div>
<h1>Live Auctions {{ unixTime }}</h1>
<button #click="tempSetAuction()">set auctions</button>
<button #click="tempClearAuction()">CLEAR ALL</button>
<div style="clear:both;"></div>
<br /><br />
<ul class="row">
<li class="col-lg-4" v-for="(auction, key, index) in auctions" :key="auction.id">
<div><span>{{ auction.name }} ({{ auction.id }})</span><br /></div>
<div>END TIME: <span class="end-time" ref="endTime">{{ auction.endtime }}</span><br /></div>
<div>TIME LEFT: <span class="bid-seconds" ref="bidTimeSeconds">{{ auction.time_left }}</span><br /></div>
<div>BID TIME: <span class="bid-time" ref="bidTime"></span><br /></div>
<br />
<span ref="serverTime">{{ auction.date_now }}</span><br /><!---->
<span ref="totalBids">{{ auction.total_bids }}</span><br />
<span ref="user">{{ auction.username }}</span><br />
<div ref="newBid" class="button">
<button #click="bidOnThis(auction.id, key)">Bid on this item</button>
</div>
<button #click="countDown()">Countdown</button><br /><br />
<hr />
</li>
</ul>
</div>
</template>
<script>
export default {
// Probably remove this
props : {
items: []
},
data() {
return {
auctions: [],
newBid: '',
totalBids: '',
user: [],
bidTimeArray: [],
unixTime: '',
timeToUpdate: '0',
textEnded: 'Ended',
show: true
};
},
created() {
axios.get('/timenow').then(result => {
this.unixTime = result.data;
});
axios.get('/auctions').then(result => {
// Set up the remaining seconds for each auction on load
this.auctions = result.data;
for (let i = 0; i < this.auctions.length; i++){
this.bidTimeArray[i] = this.auctions[i].bid_time -1;
if(this.auctions[i].endtime <= this.unixTime){
this.auctions[i].time_left = this.textEnded;
this.auctions[i].bidTime = this.textEnded;
} else {
this.auctions[i].time_left = this.auctions[i].endtime - this.unixTime;
}
}
});
axios.get('/getuser').then(result => {
this.user = result.data;
});
},
methods: {
_padNumber: number => (number > 9 || number === 0) ? number : "0" + number,
_readableTimeFromSeconds: function(seconds) {
const hours = 3600 > seconds ? 0 : parseInt(seconds / 3600, 10);
return {
hours: this._padNumber(hours),
seconds: this._padNumber(seconds % 60),
minutes: this._padNumber(parseInt(seconds / 60, 10) % 60),
}
},
bidOnThis(id, key) {
if(this.$refs.bidTimeSeconds[key].innerHTML >= 0){
axios.post('/auctions', { id: id, key: key });
//alert(+this.bidTimeArray[key] + +this.unixTime);
this.auctions[key].endtime = +this.bidTimeArray[key] + +this.unixTime;
this.auctions[key].total_bids = parseInt(this.auctions[key].total_bids) + 1;
//this.$refs.totalBids[key].innerHTML = parseInt(this.$refs.totalBids[key].innerHTML) + 1 ;
this.auctions[key].username = this.user.username ;
}
},
countDown(){
this.unixTime = this.unixTime+1;
this.timeToUpdate = this.timeToUpdate+1;
if(this.timeToUpdate >= 60){
this.timeToUpdate = 0;
axios.get('/timenow').then(result => {
this.unixTime = result.data;
//console.log('Just updated the time');
});
}
if(this.auctions.length >0){
for (let i = 0; i < this.auctions.length; i++){
if(typeof this.auctions[i].time_left == 'number' && this.auctions[i].endtime >= this.unixTime){
//if(this.auctions[i].endtime <= this.unixTime){
this.auctions[i].time_left = this.auctions[i].endtime - this.unixTime;
var newTime = parseInt(this.$refs.bidTimeSeconds[i].innerHTML);
this.$refs.bidTime[i].innerHTML = this._readableTimeFromSeconds(newTime).minutes+ ':'+this._readableTimeFromSeconds(newTime).seconds;
} else {
this.$refs.bidTime[i].innerHTML = this.textEnded;
this.$refs.newBid[i].innerHTML = '';
}
}
}
},
tempSetAuction(){
axios.get('/auctions/set').then(result => {
});
},
tempClearAuction(){
axios.get('/auctions/clear').then(result => {
});
}
},
mounted: function () {
window.setInterval(() => {
this.countDown();
},1000);
}
};
Not the complete solution. It's just the idea that I'm providing here. You can add the styles of transition. I hope that can guide you
Template:
<div id="list-demo">
<button v-on:click="add">Add</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">{{ item }}</span>
</transition-group>
</div>
ViewModel
data: {
items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
nextNum: 10
},
methods: {
add: function() {
this.items.push(this.nextNum++);
}
}
Style
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active below version 2.1.8 */
{
opacity: 0;
transform: translateY(30px); //Enter your transition transforms here
}

vue set img height based on width with dynamic styling

I have an image that should have 50% height of its width.
<img :src="post.image" ref="image" :style="{ height: imageHeight + 'px' }" />
imageHeight() {
let image = this.$refs.image
if(!image) return 0
let height = image.clientWidth * 0.5
return height
}
Unfortunately image is undefined during the evaluation of imageHeight and it does not get reevaluated when the width changes. Is there some way to make it work with a watcher or some other way?
You can use the load event to set a variable. It looks like you're using a computed, but there's no data change for it to respond to.
new Vue({
el: '#app',
data: {
url: 'http://via.placeholder.com/200x200',
imageHeight: null
},
methods: {
setheight(event) {
let image = event.target;
this.imageHeight = image.clientWidth * 0.5;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<img :src="url" #load="setheight" :style="{ height: imageHeight + 'px' }">
<div>{{imageHeight}}</div>
</div>
You could also do
<div :style={ height: myComputedHeight + '%' }></div>
data() {
return {
myCount: 10,
myTotal: 100
};
},
computed: {
myComputedHeight() {
return Math.round((this.myCount / this.myTotal) * 100);
}
}
I had to find a solution to something similar making a square div.
new Vue({
el: "#app",
data: {
initialWidth: 100,
matchedWidth: null
},
mounted() {
this.matchWidth();
},
methods: {
matchWidth() {
this.matchedWidth = this.$refs.box.offsetWidth;
}
},
computed: {
igCardStyle() {
return {
width: `${this.initialWidth}%`,
height: `${this.matchedWidth}px`
};
}
}
});
.box-wrapper {
width: 200px;
}
.box {
background-color: red;
/* border: 1px solid black; */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" ref="app">
<div class="box-wrapper">
<div class="box" :style="igCardStyle" ref="box">
</div>
</div>
<hr>
<p>width: {{initialWidth}}% of .box-wrapper or {{matchedWidth}}px</p>
<p>height: {{matchedWidth}}px</p>
</div>
In this cas you have to watch out for borders present in your $refs.box that could vary the result.
If you need the height to be the half of the width try:
calc(${this.matchedWidth} * 0.5)px on the computed style property.
Hope it helps! BR