Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ extends:

globals:
OC: readonly
n: readonly
t: readonly

rules:
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 19 additions & 4 deletions src/components/FormComponents/EditTimeField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
placeholder="00"
@input="handleInput"
/>
<span>:</span>
<input
v-model="seconds"
type="number"
min="0"
max="59"
placeholder="00"
@input="handleInput"
/>
</fieldset>
</template>

Expand All @@ -30,7 +39,7 @@ const props = defineProps({
type: Object,
required: true,
default: () => ({
time: [null, null],
time: [null, null, null],
paddedTime: null,
}),
},
Expand All @@ -48,26 +57,32 @@ const hours = ref(null);
* @type {import('vue').Ref<number>}
*/
const minutes = ref(null);
/**
* @type {import('vue').Ref<number>}
*/
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;
}
};

Expand Down
42 changes: 27 additions & 15 deletions src/components/RecipeEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,21 @@
/>
<EditTimeField
v-model="prepTime"
:field-label="t('cookbook', 'Preparation time (hours:minutes)')"
:field-label="
t('cookbook', 'Preparation time (hours:minutes:seconds)')
"
/>
<EditTimeField
v-model="cookTime"
:field-label="t('cookbook', 'Cooking time (hours:minutes)')"
:field-label="
t('cookbook', 'Cooking time (hours:minutes:seconds)')
"
/>
<EditTimeField
v-model="totalTime"
:field-label="t('cookbook', 'Total time (hours:minutes)')"
:field-label="
t('cookbook', 'Total time (hours:minutes:seconds)')
"
/>
<EditMultiselect
v-model="recipe['recipeCategory']"
Expand Down Expand Up @@ -221,9 +227,9 @@ const formDirty = ref(false);
* @type {import('vue').Ref<boolean>}
*/
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<boolean>}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
};

Expand Down
118 changes: 103 additions & 15 deletions src/components/RecipeView/RecipeTimer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,25 @@
@click="timerToggle"
></button>
<h4>{{ label }}</h4>
<p>{{ displayTime }}</p>
<div class="timeContainer">
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="displayTime"></p>
</div>
</div>
</template>

<script setup>
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: {
Expand Down Expand Up @@ -51,7 +55,7 @@ const seconds = ref(0);
/**
* @type {import('vue').Ref<boolean>}
*/
const showFullTime = ref(false);
const countdownStarted = ref(false);
// Create a ref for the audio element
const audio = ref(new Audio());

Expand All @@ -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 = () => {
Expand All @@ -88,7 +96,7 @@ const onTimerEnd = () => {
audio.value.pause();

countdown.value = null;
showFullTime.value = false;
countdownStarted.value = false;
resetTimeDisplay();
}, 100);
};
Expand All @@ -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(() => {
Expand All @@ -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 = `<span class="timerUnit">${helper.escapeHTML(unit)}</span>`;
// 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;
});
Expand Down Expand Up @@ -172,6 +247,8 @@ export default {
<style scoped>
.time {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
border: 1px solid var(--color-border-dark);
border-radius: 3px;
Expand All @@ -196,10 +273,21 @@ export default {
font-weight: bold;
}

.time p {
.time > .timeContainer {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
padding: 0.5rem;
}

/* The :deep selector prevents a data attribute from being added to the scoped element. */
.time :deep(.timerUnit) {
color: var(--color-text-lighter);
font-size: 0.8em;
margin-inline-end: 0.3em;
}

@media print {
button {
display: none !important;
Expand Down
Loading