diff --git a/.eslintrc.yml b/.eslintrc.yml index 32f545dfd..82cdc5fcd 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -12,6 +12,7 @@ extends: globals: OC: readonly + n: readonly t: readonly rules: diff --git a/CHANGELOG.md b/CHANGELOG.md index c7fb91dc1..5e4327947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ ### Added - Toast with success/error message after trying to copy ingredients [#2040](https://github.com/nextcloud/cookbook/pull/2040) @seyfeb +- Seconds can now be specified for recipe times [#2014](https://github.com/nextcloud/cookbook/pull/2014) @seyfeb ### Fixed - Prevent yield calculation for ## as ingredient headline [#1998](https://github.com/nextcloud/cookbook/pull/1998) @j0hannesr0th +- Improved styling of times in recipe view [#2014](https://github.com/nextcloud/cookbook/pull/2014) @seyfeb - Add missing translatable string for recipe-creation button in empty list view [#2015](https://github.com/nextcloud/cookbook/pull/2015) @seyfeb ### Documentation diff --git a/src/components/FormComponents/EditTimeField.vue b/src/components/FormComponents/EditTimeField.vue index d3525fe1b..70de77d03 100644 --- a/src/components/FormComponents/EditTimeField.vue +++ b/src/components/FormComponents/EditTimeField.vue @@ -17,6 +17,15 @@ placeholder="00" @input="handleInput" /> + : + @@ -30,7 +39,7 @@ const props = defineProps({ type: Object, required: true, default: () => ({ - time: [null, null], + time: [null, null, null], paddedTime: null, }), }, @@ -48,26 +57,32 @@ const hours = ref(null); * @type {import('vue').Ref} */ const minutes = ref(null); +/** + * @type {import('vue').Ref} + */ +const seconds = ref(null); // Methods const handleInput = () => { + seconds.value = seconds.value ? seconds.value : 0; minutes.value = minutes.value ? minutes.value : 0; hours.value = hours.value ? hours.value : 0; // create padded time string const hoursPadded = hours.value.toString().padStart(2, '0'); const minutesPadded = minutes.value.toString().padStart(2, '0'); + const secondsPadded = seconds.value.toString().padStart(2, '0'); emit('input', { - time: [hours.value, minutes.value], - paddedTime: `PT${hoursPadded}H${minutesPadded}M`, + time: [hours.value, minutes.value, seconds.value], + paddedTime: `PT${hoursPadded}H${minutesPadded}M${secondsPadded}S`, }); }; const setLocalValueFromProps = () => { if (props.value?.time) { - [hours.value, minutes.value] = props.value.time; + [hours.value, minutes.value, seconds.value] = props.value.time; } }; diff --git a/src/components/RecipeEdit.vue b/src/components/RecipeEdit.vue index 45a641e94..00b11a1e3 100644 --- a/src/components/RecipeEdit.vue +++ b/src/components/RecipeEdit.vue @@ -27,15 +27,21 @@ /> } */ const savingRecipe = ref(false); -const prepTime = ref({ time: [0, 0], paddedTime: '' }); -const cookTime = ref({ time: [0, 0], paddedTime: '' }); -const totalTime = ref({ time: [0, 0], paddedTime: '' }); +const prepTime = ref({ time: [0, 0, 0], paddedTime: '' }); +const cookTime = ref({ time: [0, 0, 0], paddedTime: '' }); +const totalTime = ref({ time: [0, 0, 0], paddedTime: '' }); const allCategories = ref([]); /** * @type {import('vue').Ref} @@ -517,9 +523,9 @@ const save = async () => { }; const initEmptyRecipe = () => { - prepTime.value = { time: [0, 0], paddedTime: '' }; - cookTime.value = { time: [0, 0], paddedTime: '' }; - totalTime.value = { time: [0, 0], paddedTime: '' }; + prepTime.value = { time: [0, 0, 0], paddedTime: '' }; + cookTime.value = { time: [0, 0, 0], paddedTime: '' }; + totalTime.value = { time: [0, 0, 0], paddedTime: '' }; // this.nutrition = {} recipe.value = { id: 0, @@ -563,27 +569,33 @@ const setup = async () => { if (route.params.id) { // Parse time values let timeComps = recipe.value.prepTime - ? recipe.value.prepTime.match(/PT(\d+?)H(\d+?)M/) + ? recipe.value.prepTime.match(/PT(\d+?)H(\d+?)M(\d+?)S/) : null; prepTime.value = { - time: timeComps ? [timeComps[1], timeComps[2]] : [0, 0], + time: timeComps + ? [timeComps[1], timeComps[2], timeComps[3]] + : [0, 0, 0], paddedTime: recipe.value.prepTime, }; timeComps = recipe.value.cookTime - ? recipe.value.cookTime.match(/PT(\d+?)H(\d+?)M/) + ? recipe.value.cookTime.match(/PT(\d+?)H(\d+?)M(\d+?)S/) : null; cookTime.value = { - time: timeComps ? [timeComps[1], timeComps[2]] : [0, 0], + time: timeComps + ? [timeComps[1], timeComps[2], timeComps[3]] + : [0, 0, 0], paddedTime: recipe.value.cookTime, }; timeComps = recipe.value.totalTime - ? recipe.value.totalTime.match(/PT(\d+?)H(\d+?)M/) + ? recipe.value.totalTime.match(/PT(\d+?)H(\d+?)M(\d+?)S/) : null; totalTime.value = { - time: timeComps ? [timeComps[1], timeComps[2]] : [0, 0], + time: timeComps + ? [timeComps[1], timeComps[2], timeComps[3]] + : [0, 0, 0], paddedTime: recipe.value.totalTime, }; diff --git a/src/components/RecipeView/RecipeTimer.vue b/src/components/RecipeView/RecipeTimer.vue index ba414a9dd..7a40b6276 100644 --- a/src/components/RecipeView/RecipeTimer.vue +++ b/src/components/RecipeView/RecipeTimer.vue @@ -7,7 +7,10 @@ @click="timerToggle" >

{{ label }}

-

{{ displayTime }}

+
+ +

+
@@ -15,13 +18,14 @@ import { computed, defineProps, onMounted, ref, watch } from 'vue'; import { linkTo } from '@nextcloud/router'; import { showSimpleAlertModal } from 'cookbook/js/modals'; +import helper from 'cookbook/js/helper'; // Properties const props = defineProps({ value: { type: Object, default() { - return { hours: 0, minutes: 0 }; + return { hours: 0, minutes: 0, seconds: 0 }; }, }, label: { @@ -51,7 +55,7 @@ const seconds = ref(0); /** * @type {import('vue').Ref} */ -const showFullTime = ref(false); +const countdownStarted = ref(false); // Create a ref for the audio element const audio = ref(new Audio()); @@ -67,7 +71,11 @@ const resetTimeDisplay = () => { } else { minutes.value = 0; } - seconds.value = 0; + if (props.value.seconds) { + seconds.value = parseInt(props.value.seconds, 10); + } else { + seconds.value = 0; + } }; const onTimerEnd = () => { @@ -88,7 +96,7 @@ const onTimerEnd = () => { audio.value.pause(); countdown.value = null; - showFullTime.value = false; + countdownStarted.value = false; resetTimeDisplay(); }, 100); }; @@ -97,8 +105,8 @@ const timerToggle = () => { // We will switch to full time display the first time this method is invoked. // There should probably also be a way to reset the timer other than by letting // it run its course... - if (!showFullTime.value) { - showFullTime.value = true; + if (!countdownStarted.value) { + countdownStarted.value = true; } if (countdown.value === null) { countdown.value = window.setInterval(() => { @@ -125,17 +133,84 @@ const timerToggle = () => { } }; +/** + * Add style to the unit of the value. + * @param str Complete translated string with value and unit + * @param value The value without the unit + * @param isPadded If value should be padded with zeros to ensure it has two digits + * @returns {string} Complete styled string with value and unit + */ +const styleUnit = (str, value, isPadded = true) => { + // Remove value + const unit = str.replace(`${value.toString()}`, ''); + // Style unit + let text = `${helper.escapeHTML(unit)}`; + // Reassemble value and unit. + // Make sure that value is a number to prevent XSS attacks (due to the v-html directive) + if (isPadded) { + text = `${Number(value).toString().padStart(2, '0')}${text}`; + } else { + text = `${Number(value).toString()}${text}`; + } + return text; +}; + // Computed properties +const displayHours = computed(() => { + let hoursText = ''; + // TRANSLATORS hours part of timer text + hoursText += n('cookbook', '{hours}h', '{hours}h', hours.value, { + hours: `${hours.value.toString()}`, + }); + + return styleUnit(hoursText, hours.value); +}); + +const displayMinutes = computed(() => { + let minutesText = ''; + // TRANSLATORS minutes part of timer text + minutesText += n('cookbook', '{minutes}m', '{minutes}m', minutes.value, { + minutes: `${minutes.value.toString()}`, + }); + + return styleUnit(minutesText, minutes.value); +}); + +const displaySeconds = computed(() => { + let secondsText = ''; + // TRANSLATORS seconds part of timer text + secondsText += n('cookbook', '{seconds}s', '{seconds}s', seconds.value, { + seconds: `${seconds.value.toString()}`, + }); + + return styleUnit(secondsText, seconds.value); +}); + const displayTime = computed(() => { let text = ''; - if (showFullTime.value) { - text += `${hours.value.toString().padStart(2, '0')}:`; - } else { - text += `${hours.value.toString()}:`; + if (props.value.hours && props.value.hours > 0) { + text += displayHours.value; + } + if ( + (countdownStarted.value && + ((props.value.hours && props.value.hours > 0) || + (props.value.minutes && props.value.minutes > 0))) || + (!countdownStarted.value && + props.value.minutes && + props.value.minutes > 0) + ) { + text += displayMinutes.value; } - text += minutes.value.toString().padStart(2, '0'); - if (showFullTime.value) { - text += `:${seconds.value.toString().padStart(2, '0')}`; + if ( + (countdownStarted.value && + ((props.value.hours && props.value.hours > 0) || + (props.value.minutes && props.value.minutes > 0) || + (props.value.seconds && props.value.seconds > 0))) || + (!countdownStarted.value && + props.value.seconds && + props.value.seconds > 0) + ) { + text += displaySeconds.value; } return text; }); @@ -172,6 +247,8 @@ export default {