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 {