mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-03 05:11:31 -05:00
proper timer component
This commit is contained in:
@@ -2,45 +2,35 @@
|
|||||||
|
|
||||||
<template v-if="recipe.name != undefined">
|
<template v-if="recipe.name != undefined">
|
||||||
|
|
||||||
|
|
||||||
<v-card class="mt-md-4">
|
<v-card class="mt-md-4">
|
||||||
|
|
||||||
<v-img max-height="25vh" cover lazy :src="recipe.image" v-if="recipe.image != undefined" class="align-end">
|
<v-img max-height="25vh" cover lazy :src="recipe.image" v-if="recipe.image != undefined" class="align-end">
|
||||||
<KeywordsComponent variant="flat" class="ms-1 mb-2" :keywords="recipe.keywords"></KeywordsComponent>
|
<KeywordsComponent variant="flat" class="ms-1 mb-2" :keywords="recipe.keywords"></KeywordsComponent>
|
||||||
</v-img>
|
</v-img>
|
||||||
|
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-sheet class="d-flex align-center">
|
<v-sheet class="d-flex align-center">
|
||||||
<span class="ps-2 text-h5 text-truncate flex-grow-1">{{ recipe.name }}</span>
|
<span class="ps-2 text-h5 flex-grow-1" :class="{'text-truncate': !showFullRecipeName}" @click="showFullRecipeName = !showFullRecipeName">{{ recipe.name }}</span>
|
||||||
<recipe-context-menu :recipe="recipe"></recipe-context-menu>
|
<recipe-context-menu :recipe="recipe"></recipe-context-menu>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- <v-card class="mt-1">-->
|
|
||||||
<!-- <v-sheet class="d-flex ">-->
|
|
||||||
<!-- <span class="ps-2 text-h5 flex-grow-1">{{ recipe.name }}</span>-->
|
|
||||||
<!-- <recipe-context-menu :recipe="recipe"></recipe-context-menu>-->
|
|
||||||
<!-- </v-sheet>-->
|
|
||||||
<!-- </v-card>-->
|
|
||||||
|
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-card class="mt-1">
|
<v-card class="mt-1">
|
||||||
<v-container>
|
<v-container>
|
||||||
<v-row class="text-center text-body-2">
|
<v-row class="text-center text-body-2">
|
||||||
<v-col class="pt-1 pb-1">
|
<v-col class="pt-1 pb-1">
|
||||||
<i class="fas fa-cogs"></i> {{ recipe.workingTime }} min<br/>
|
<i class="fas fa-cogs fa-fw mr-1"></i> {{ recipe.workingTime }} min<br/>
|
||||||
<div class="text-grey">Working Time</div>
|
<div class="text-grey">Working Time</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col class="pt-1 pb-1">
|
<v-col class="pt-1 pb-1">
|
||||||
<div><i class="fas fa-hourglass-half"></i> {{ recipe.waitingTime }} min</div>
|
<div><i class="fas fa-hourglass-half fa-fw mr-1"></i> {{ recipe.waitingTime }} min</div>
|
||||||
<div class="text-grey">Waiting Time</div>
|
<div class="text-grey">Waiting Time</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col class="pt-1 pb-1">
|
<v-col class="pt-1 pb-1">
|
||||||
<NumberScalerDialog :number="servings" @change="servings = $event.number" title="Servings">
|
<NumberScalerDialog :number="servings" @change="servings = $event.number" title="Servings">
|
||||||
<template #activator>
|
<template #activator>
|
||||||
<div class="cursor-pointer">
|
<div class="cursor-pointer">
|
||||||
<i class="fas fa-calendar-alt"></i> {{ servings }} <br/>
|
<i class="fas fa-sort-numeric-up fa-fw mr-1"></i> {{ servings }} <br/>
|
||||||
<div class="text-grey"><span v-if="recipe?.servingsText">{{ recipe.servingsText }}</span><span v-else>Servings</span></div>
|
<div class="text-grey"><span v-if="recipe?.servingsText">{{ recipe.servingsText }}</span><span v-else>Servings</span></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -86,6 +76,7 @@ export default defineComponent({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
servings: 1,
|
servings: 1,
|
||||||
|
showFullRecipeName: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
@@ -5,29 +5,14 @@
|
|||||||
<v-col>{{ step.name }}</v-col>
|
<v-col>{{ step.name }}</v-col>
|
||||||
<v-col class="text-right">
|
<v-col class="text-right">
|
||||||
<v-btn-group density="compact" variant="tonal">
|
<v-btn-group density="compact" variant="tonal">
|
||||||
<v-btn size="small" color="info" v-if="step.time != undefined && step.time > 0" @click="startTimer(step.time)"><i class="fas fa-stopwatch mr-1"></i> {{ step.time }}</v-btn>
|
<v-btn size="small" color="info" v-if="step.time != undefined && step.time > 0" @click="timerRunning = true"><i class="fas fa-stopwatch mr-1"></i> {{ step.time }}</v-btn>
|
||||||
<v-btn size="small" color="success"><i class="fas fa-check"></i></v-btn>
|
<v-btn size="small" color="success"><i class="fas fa-check"></i></v-btn>
|
||||||
</v-btn-group>
|
</v-btn-group>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
|
<timer :seconds="step.time != undefined ? step.time*60 : 0" @stop="timerRunning = false" v-if="timerRunning"></timer>
|
||||||
<v-progress-linear v-if="timer_end != null" :model-value="timerProgress" color="success" height="5">
|
|
||||||
|
|
||||||
</v-progress-linear>
|
|
||||||
<v-alert :color="timer_color" closable @click:close="timer_end = null">
|
|
||||||
<v-alert-title><i class="fas fa-stopwatch mr-1"></i> {{ remaining_time }}</v-alert-title>
|
|
||||||
Finished at {{ finished_at }}
|
|
||||||
<template #close>
|
|
||||||
<v-btn-group divided>
|
|
||||||
<v-btn width="40" @click="subTimer"><i class="fas fa-minus"></i>1</v-btn>
|
|
||||||
<v-btn width="40" @click="addTimer"><i class="fas fa-plus"></i>1</v-btn>
|
|
||||||
<v-btn width="40"><i class="fas fa-pause"></i></v-btn>
|
|
||||||
<v-btn width="40"><i class="fas fa-stop"></i></v-btn>
|
|
||||||
</v-btn-group>
|
|
||||||
</template>
|
|
||||||
</v-alert>
|
|
||||||
|
|
||||||
<IngredientsTable :ingredients="step.ingredients"></IngredientsTable>
|
<IngredientsTable :ingredients="step.ingredients"></IngredientsTable>
|
||||||
|
|
||||||
@@ -44,38 +29,14 @@ import {Step} from "@/openapi";
|
|||||||
import {DateTime, Duration, Interval} from "luxon";
|
import {DateTime, Duration, Interval} from "luxon";
|
||||||
|
|
||||||
import Instructions from "@/components/display/Instructions.vue";
|
import Instructions from "@/components/display/Instructions.vue";
|
||||||
|
import Timer from "@/components/display/Timer.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Step",
|
name: "Step",
|
||||||
computed: {
|
computed: {
|
||||||
timer_color: function () {
|
|
||||||
if (this.timer_end != null) {
|
|
||||||
if (this.time_now > this.timer_end) {
|
|
||||||
return 'warning'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
},
|
|
||||||
timerProgress: function () {
|
|
||||||
if (this.timer_end != null && this.timer_start != null && this.step.time != undefined) {
|
|
||||||
return (Interval.fromDateTimes(this.timer_start, this.time_now).length('seconds') / (this.step.time * 60)) * 100
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
},
|
|
||||||
remaining_time: function () {
|
|
||||||
if (this.timer_end != null) {
|
|
||||||
return Duration.fromMillis(Interval.fromDateTimes(this.time_now, this.timer_end).length()).toFormat('hh:mm:ss')
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
},
|
|
||||||
finished_at: function () {
|
|
||||||
if (this.timer_end != null) {
|
|
||||||
return this.timer_end.toLocaleString(DateTime.TIME_SIMPLE)
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
components: {Instructions, IngredientsTable},
|
components: {Timer, Instructions, IngredientsTable},
|
||||||
props: {
|
props: {
|
||||||
step: {
|
step: {
|
||||||
type: {} as PropType<Step>,
|
type: {} as PropType<Step>,
|
||||||
@@ -88,29 +49,14 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
timer_end: null as null | DateTime,
|
timerRunning: false,
|
||||||
timer_start: null as null | DateTime,
|
|
||||||
time_now: DateTime.now(),
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
setInterval(() => {
|
|
||||||
this.time_now = DateTime.now()
|
|
||||||
}, 500)
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
startTimer(minutes: number) {
|
|
||||||
this.timer_start = DateTime.now()
|
|
||||||
this.timer_end = DateTime.now().plus({minutes: minutes})
|
|
||||||
},
|
|
||||||
subTimer(){
|
|
||||||
if (this.timer_end != null){
|
|
||||||
this.timer_end = this.timer_end.minus({minutes: 1})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addTimer(){
|
|
||||||
this.timer_end?.plus({minutes: 1})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
83
vue3/src/components/display/Timer.vue
Normal file
83
vue3/src/components/display/Timer.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<v-progress-linear :model-value="timerProgress" color="primary" height="5"></v-progress-linear>
|
||||||
|
<v-alert :color="timerColor" class="rounded-0" variant="tonal">
|
||||||
|
<v-alert-title><i class="fas fa-stopwatch mr-1"></i> {{ Duration.fromMillis(durationSeconds * 1000).toFormat('hh:mm:ss') }}</v-alert-title>
|
||||||
|
Finished at {{ DateTime.now().plus({'seconds': durationSeconds}).toLocaleString(DateTime.TIME_SIMPLE) }}
|
||||||
|
<template #close>
|
||||||
|
<v-btn-group divided>
|
||||||
|
<v-btn width="40" @click="changeTimer(-60)"><i class="fas fa-minus"></i>1</v-btn>
|
||||||
|
<v-btn width="40" @click="changeTimer(+60)"><i class="fas fa-plus"></i>1</v-btn>
|
||||||
|
<v-btn width="40" @click="timerRunning = !timerRunning"><i class="fas fa-fw" :class="{'fa-pause': timerRunning, 'fa-play': !timerRunning}"></i></v-btn>
|
||||||
|
<v-btn width="40" @click="stopTimer()"><i class="fas fa-stop"></i></v-btn>
|
||||||
|
</v-btn-group>
|
||||||
|
</template>
|
||||||
|
</v-alert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits(['stop'])
|
||||||
|
|
||||||
|
import {computed, onMounted, ref} from "vue";
|
||||||
|
import {DateTime, Duration} from "luxon";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
seconds: {type: Number, required: true}
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialDurationSeconds = ref(props.seconds)
|
||||||
|
const durationSeconds = ref(initialDurationSeconds.value)
|
||||||
|
const timerRunning = ref(true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change timer color based on if the timer has time left or if its finished
|
||||||
|
*/
|
||||||
|
let timerColor = computed(() => {
|
||||||
|
return (durationSeconds.value > 0) ? 'primary' : 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* calculate timer progress based on initial time and remaining time
|
||||||
|
*/
|
||||||
|
const timerProgress = computed(() => {
|
||||||
|
if (initialDurationSeconds.value == 0) {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
return (1 - (durationSeconds.value / initialDurationSeconds.value)) * 100
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lifecycle hook onMounted
|
||||||
|
* start interval running the timer
|
||||||
|
*/
|
||||||
|
onMounted(() => {
|
||||||
|
setInterval(() => {
|
||||||
|
if (timerRunning.value && durationSeconds.value > 0) {
|
||||||
|
durationSeconds.value = durationSeconds.value - 1
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* utility function to change the timer duration
|
||||||
|
* also changes initial duration to keep progress correct
|
||||||
|
* @param seconds number of seconds to change (positive to add time, negative to remove time)
|
||||||
|
*/
|
||||||
|
function changeTimer(seconds: number) {
|
||||||
|
durationSeconds.value = Math.max(0, durationSeconds.value + seconds)
|
||||||
|
initialDurationSeconds.value = Math.max(0, initialDurationSeconds.value + seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stops the timer emitting the stop event and resetting the time to time initially passed via prop so it can be started again
|
||||||
|
*/
|
||||||
|
function stopTimer() {
|
||||||
|
durationSeconds.value = props.seconds
|
||||||
|
initialDurationSeconds.value = props.seconds
|
||||||
|
emit('stop')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user