style: Update prettier, eslint, stylelint config. Switch to single quotes and require semicolons in JS code.

Run stylelint and prettier to fix style

Signed-off-by: Sebastian Fey <info@sebastianfey.de>
This commit is contained in:
Sebastian Fey 2023-11-18 17:29:26 +01:00
Родитель 776b0f05ce
Коммит af2a55becb
48 изменённых файлов: 2680 добавлений и 4019 удалений

Просмотреть файл

@ -18,6 +18,17 @@ rules:
no-plusplus:
- error
- allowForLoopAfterthoughts: true
quotes:
- error
- single
- avoidEscape: true
string-quotes:
- error
- single
- avoidEscape: true
selector-attribute-quotes:
- error
- always
root: true

Просмотреть файл

@ -1,4 +1,5 @@
{
"semi": false,
"tabWidth": 4
"semi": true,
"tabWidth": 4,
"singleQuote": true
}

4795
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -62,7 +62,7 @@
<template #icon>
<PencilIcon :size="20" />
</template>
{{ t("cookbook", "Edit") }}
{{ t('cookbook', 'Edit') }}
</NcButton>
<NcButton
v-if="isEdit || isCreate"
@ -78,7 +78,7 @@
/>
<CheckmarkIcon v-else :size="20" />
</template>
{{ t("cookbook", "Save") }}
{{ t('cookbook', 'Save') }}
</NcButton>
<!-- This is clumsy design but the component cannot display just one input element on the breadcrumbs bar -->
<NcActions
@ -92,10 +92,10 @@
:value="filterValue"
@update:value="updateFilters"
>
{{ t("cookbook", "Filter") }}
{{ t('cookbook', 'Filter') }}
</NcActionInput>
<NcActionInput icon="icon-search" @submit="search">
{{ t("cookbook", "Search") }}
{{ t('cookbook', 'Search') }}
</NcActionInput>
</NcActions>
{{/* Overflow buttons (3-dot menu) */}}
@ -115,7 +115,7 @@
:aria-label="t('cookbook', 'Reload recipe')"
@click="reloadRecipeEdit()"
>
{{ t("cookbook", "Reload recipe") }}
{{ t('cookbook', 'Reload recipe') }}
</NcActionButton>
<NcActionButton
v-if="isEdit"
@ -123,7 +123,7 @@
:aria-label="t('cookbook', 'Abort editing')"
@click="goToRecipe(store.state.recipe.id)"
>
{{ t("cookbook", "Abort editing") }}
{{ t('cookbook', 'Abort editing') }}
<template #icon>
<NcLoadingIcon
v-if="
@ -146,7 +146,7 @@
:aria-label="t('cookbook', 'Reload recipe')"
@click="reloadRecipeView()"
>
{{ t("cookbook", "Reload recipe") }}
{{ t('cookbook', 'Reload recipe') }}
</NcActionButton>
<NcActionButton
v-if="isRecipe"
@ -155,7 +155,7 @@
@click="printRecipe()"
>
<template #icon=""><printer-icon :size="20" /></template>
{{ t("cookbook", "Print recipe") }}
{{ t('cookbook', 'Print recipe') }}
</NcActionButton>
<NcActionButton
v-if="isRecipe"
@ -164,7 +164,7 @@
:aria-label="t('cookbook', 'Delete recipe')"
@click="deleteRecipe()"
>
{{ t("cookbook", "Delete recipe") }}
{{ t('cookbook', 'Delete recipe') }}
</NcActionButton>
</NcActions>
</div>
@ -179,38 +179,38 @@ export default {
<script setup>
import { computed, getCurrentInstance, ref } from 'vue';
import { useRoute } from 'vue-router/composables';
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
// Cannot use `Button` else get `vue/no-reserved-component-names` eslint errors
import NcButton from "@nextcloud/vue/dist/Components/NcButton";
import NcActionInput from "@nextcloud/vue/dist/Components/NcActionInput";
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon";
import NcButton from '@nextcloud/vue/dist/Components/NcButton';
import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput';
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
import PencilIcon from "icons/Pencil.vue";
import LoadingIcon from "icons/Loading.vue";
import CheckmarkIcon from "icons/Check.vue";
import PrinterIcon from "icons/Printer.vue";
import EyeIcon from "icons/Eye.vue";
import PencilIcon from 'icons/Pencil.vue';
import LoadingIcon from 'icons/Loading.vue';
import CheckmarkIcon from 'icons/Check.vue';
import PrinterIcon from 'icons/Printer.vue';
import EyeIcon from 'icons/Eye.vue';
import helpers from "cookbook/js/helper";
import helpers from 'cookbook/js/helper';
import {
showSimpleAlertModal,
showSimpleConfirmModal,
} from "cookbook/js/modals";
} from 'cookbook/js/modals';
import Location from "./Location.vue";
import ModeIndicator from "./ModeIndicator.vue";
import Location from './Location.vue';
import ModeIndicator from './ModeIndicator.vue';
import { useStore } from '../../store';
import emitter from '../../bus';
const route = useRoute();
const store = useStore();
const filterValue = ref("");
const filterValue = ref('');
/** Computed values **/
const isCreate = computed(() => {
return store.state.page === "create";
return store.state.page === 'create';
});
const isEdit = computed(() => {
// A recipe is being loaded
@ -218,16 +218,14 @@ const isEdit = computed(() => {
return false; // Do not show both at the same time
}
// Editing requires that a recipe was found
return !!(
store.state.page === "edit" && store.state.recipe
);
return !!(store.state.page === 'edit' && store.state.recipe);
});
const isIndex = computed(() => {
// A recipe is being loaded
if (!!store.state.loadingRecipe) {
return false; // Do not show both at the same time
}
return store.state.page === "index";
return store.state.page === 'index';
});
const isLoading = computed(() => {
// The page is being loaded
@ -239,38 +237,36 @@ const isRecipe = computed(() => {
return false; // Do not show both at the same time
}
// Viewing recipe requires that one was found
return !!(
store.state.page === "recipe" && store.state.recipe
);
return !!(store.state.page === 'recipe' && store.state.recipe);
});
const isSearch = computed(() => {
// A recipe is being loaded
if (!!store.state.loadingRecipe) {
return false // Do not show both at the same time
return false; // Do not show both at the same time
}
return store.state.page === "search";
return store.state.page === 'search';
});
const pageNotFound = computed(() => {
return store.state.page === "notfound";
return store.state.page === 'notfound';
});
const recipeNotFound = computed(() => {
// Editing or viewing recipe was attempted, but no recipe was found
return (
["edit", "recipe"].indexOf(store.state.page) !== -1 &&
['edit', 'recipe'].indexOf(store.state.page) !== -1 &&
!store.state.recipe
);
});
const searchTitle = computed(() => {
if (route.name === "search-category") {
return t("cookbook", "Category")
if (route.name === 'search-category') {
return t('cookbook', 'Category');
}
if (route.name === "search-name") {
return t("cookbook", "Recipe name")
if (route.name === 'search-name') {
return t('cookbook', 'Recipe name');
}
if (route.name === "search-tags") {
return t("cookbook", "Tags")
if (route.name === 'search-tags') {
return t('cookbook', 'Tags');
}
return t("cookbook", "Search for recipes");
return t('cookbook', 'Search for recipes');
});
// Methods
@ -283,18 +279,18 @@ const deleteRecipe = async () => {
t("cookbook", "Are you sure you want to delete this recipe?"),
))
) {
return
return;
}
try {
await store.dispatch("deleteRecipe", {
await store.dispatch('deleteRecipe', {
id: store.state.recipe.id,
})
helpers.goTo("/")
});
helpers.goTo('/');
} catch (e) {
await showSimpleAlertModal(t("cookbook", "Delete failed"))
await showSimpleAlertModal(t('cookbook', 'Delete failed'));
if (e && e instanceof Error) {
throw e
throw e;
}
}
};
@ -304,15 +300,15 @@ const printRecipe = () => {
};
const reloadRecipeEdit = () => {
emitter.emit("reloadRecipeEdit");
emitter.emit('reloadRecipeEdit');
};
const reloadRecipeView = () => {
emitter.emit("reloadRecipeView");
emitter.emit('reloadRecipeView');
};
const saveChanges = () => {
emitter.emit("saveRecipe");
emitter.emit('saveRecipe');
};
const search = (e) => {

Просмотреть файл

@ -14,6 +14,6 @@ export default {
import { defineProps } from 'vue';
const props = defineProps({
title: { type: String, default: "",}
title: { type: String, default: '' },
});
</script>

Просмотреть файл

@ -2,7 +2,6 @@
<span class="mode-indicator">{{ title }}</span>
</template>
<script>
export default {
name: 'ModeIndicator',
@ -10,9 +9,9 @@ export default {
</script>
<script setup>
import {defineProps} from 'vue';
import { defineProps } from 'vue';
defineProps({
title: { type: String, default: '' }
title: { type: String, default: '' },
});
</script>

Просмотреть файл

@ -5,10 +5,9 @@
<script>
export default {
name: 'Location',
}
};
</script>
<script setup>
import api from 'cookbook/js/api-interface';
import { computed, getCurrentInstance, onMounted, ref, watch } from 'vue';
@ -27,7 +26,7 @@ const recipes = ref([]);
* Is the Cookbook recipe directory currently being changed?
*/
const updatingRecipeDirectory = computed(() => {
return store.state.updatingRecipeDirectory
return store.state.updatingRecipeDirectory;
});
/**
@ -43,23 +42,23 @@ watch(updatingRecipeDirectory, async (newVal, oldVal) => {
onMounted(() => {
getCurrentInstance().proxy.$log.info('AppIndex mounted');
loadAll();
})
});
/**
* Load all recipes from the database
*/
const loadAll = () => {
const loadAll = () => {
api.recipes
.getAll()
.then((response) => {
recipes.value = response.data
recipes.value = response.data;
// Always set page name last
store.dispatch('setPage', { page: 'index' })
store.dispatch('setPage', { page: 'index' });
})
.catch(() => {
// Always set page name last
store.dispatch('setPage', { page: 'index' })
store.dispatch('setPage', { page: 'index' });
});
};
</script>

Просмотреть файл

@ -4,7 +4,7 @@
<div class="main">
<div class="dialog">
<div class="message">
{{ t("cookbook", "Cannot access recipe folder.") }}
{{ t('cookbook', 'Cannot access recipe folder.') }}
</div>
<div>
{{
@ -14,7 +14,7 @@
</div>
<div>
<button @click.prevent="selectFolder">
{{ t("cookbook", "Select recipe folder") }}
{{ t('cookbook', 'Select recipe folder') }}
</button>
</div>
</div>
@ -39,7 +39,7 @@ const selectFolder = () => {
this.$store
.dispatch('updateRecipeDirectory', { dir: path })
.then(() => {
window.location.reload()
window.location.reload();
});
});
};
@ -47,7 +47,7 @@ const selectFolder = () => {
<script>
export default {
name: 'InvalidGuest'
name: 'InvalidGuest',
};
</script>

Просмотреть файл

@ -38,7 +38,6 @@ const isMobile = useIsMobile();
*/
const isNavigationOpen = ref(false);
// previously there was this commented section in this component. I leave it here for reference:
// watch: {
// // This might be handy when routing of Vue components needs fixing.
@ -68,7 +67,6 @@ const updateAppNavigationOpen = ({ open }) => {
const closeNavigation = () => {
emit('toggle-navigation', { open: false });
};
</script>
<script>
@ -159,7 +157,7 @@ export default {
a:link::after,
a:visited::after {
content: " [" attr(href) "] ";
content: ' [' attr(href) '] ';
}
body {

Просмотреть файл

@ -18,7 +18,7 @@
@submit="downloadRecipe"
@update:value="updateUrl"
>
{{ t("cookbook", "Download recipe from URL") }}
{{ t('cookbook', 'Download recipe from URL') }}
</NcActionInput>
<NcAppNavigationItem
@ -53,7 +53,11 @@
<NcAppNavigationItem
v-for="(cat, idx) in categories"
:key="cat + idx"
:ref="el => { categoryItemElements[idx] = el }"
:ref="
(el) => {
categoryItemElements[idx] = el;
}
"
:name="cat.name"
:icon="'icon-category-files'"
:to="'/category/' + cat.name"
@ -63,7 +67,7 @@
@update:open="categoryOpen(idx)"
@update:title="
(val) => {
categoryUpdateName(idx, val)
categoryUpdateName(idx, val);
}
"
>
@ -84,7 +88,14 @@
</template>
<script setup>
import { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue';
import {
computed,
getCurrentInstance,
nextTick,
onMounted,
ref,
watch,
} from 'vue';
import { emit } from '@nextcloud/event-bus';
import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput';
@ -102,7 +113,7 @@ import { showSimpleAlertModal } from 'cookbook/js/modals';
import emitter from '../bus';
import { SHOW_SETTINGS_EVENT } from '../composables/useSettingsDialog';
import { useStore } from "../store";
import { useStore } from '../store';
const log = getCurrentInstance().proxy.$log;
const store = useStore();
@ -165,12 +176,15 @@ const categoryUpdating = computed(() => {
// Watchers
// Register a method hook for navigation refreshing
watch(() => refreshRequired.value, (newVal, oldVal) => {
if (newVal !== oldVal && newVal === true) {
log.debug('Calling getCategories from refreshRequired');
getCategories();
}
});
watch(
() => refreshRequired.value,
(newVal, oldVal) => {
if (newVal !== oldVal && newVal === true) {
log.debug('Calling getCategories from refreshRequired');
getCategories();
}
},
);
// Methods
/**
@ -198,7 +212,7 @@ const openCategory = async (idx) => {
const response = await api.recipes.allInCategory(cat.name);
cat.recipes = response.data;
} catch (e) {
cat.recipes = []
cat.recipes = [];
await showSimpleAlertModal(
// prettier-ignore
t('cookbook', 'Failed to load category {category} recipes',
@ -338,9 +352,7 @@ const getCategories = async () => {
}
if (categoryItemElements[i][0].opened) {
log.info(
`Reloading recipes in ${
categoryItemElements[i][0].title
}`,
`Reloading recipes in ${categoryItemElements[i][0].title}`,
);
await openCategory(i);
}
@ -349,11 +361,8 @@ const getCategories = async () => {
store.dispatch('setAppNavigationRefreshRequired', {
isRequired: false,
});
} catch (e) {
await showSimpleAlertModal(
t('cookbook', 'Failed to fetch categories'),
);
await showSimpleAlertModal(t('cookbook', 'Failed to fetch categories'));
if (e && e instanceof Error) {
throw e;
}

Просмотреть файл

@ -25,34 +25,33 @@
</template>
<script setup>
import { FilePickerType, getFilePickerBuilder } from "@nextcloud/dialogs";
import { defineProps } from "vue";
import { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs';
import { defineProps } from 'vue';
const emit = defineEmits(['input']);
const props = defineProps({
value: { type: String, default: ''},
fieldLabel: { type: String, default: ""}
value: { type: String, default: '' },
fieldLabel: { type: String, default: '' },
});
const pickImage = (e) => {
e.preventDefault()
e.preventDefault();
const filePicker = getFilePickerBuilder(
t("cookbook", "Path to your recipe image"),
t('cookbook', 'Path to your recipe image'),
)
.addMimeTypeFilter("image/jpeg")
.addMimeTypeFilter("image/png")
.addMimeTypeFilter('image/jpeg')
.addMimeTypeFilter('image/png')
.setType(FilePickerType.Choose)
.build();
filePicker.pick().then((path) => {
emit('input', path);
});
}
};
</script>
<script>
export default {
name: 'EditImageField'
name: 'EditImageField',
};
</script>

Просмотреть файл

@ -4,7 +4,9 @@
{{ fieldLabel }}
</label>
<textarea
v-if="props.fieldType === 'textarea' || props.fieldType === 'markdown'"
v-if="
props.fieldType === 'textarea' || props.fieldType === 'markdown'
"
ref="inputField"
v-model="content"
@input="handleInput"
@ -40,7 +42,7 @@
</template>
<script setup>
import { getCurrentInstance, nextTick, ref, watch } from "vue";
import { getCurrentInstance, nextTick, ref, watch } from 'vue';
import SuggestionsPopup from '../Modals/SuggestionsPopup';
import useSuggestionPopup from '../../composables/useSuggestionsPopup';
const log = getCurrentInstance().proxy.$log;
@ -50,11 +52,11 @@ const emit = defineEmits(['input']);
const props = defineProps({
fieldLabel: {
type: String,
default: "",
default: '',
},
fieldType: {
type: String,
default: "",
default: '',
},
hide: {
type: Boolean,
@ -62,13 +64,13 @@ const props = defineProps({
required: false,
},
suggestionOptions: {
type: Array
type: Array,
},
// Value (passed in v-model)
// eslint-disable-next-line vue/require-prop-types
value: {
type: String,
default: "",
default: '',
required: true,
},
});
@ -106,14 +108,25 @@ let {
handleSuggestionsPopupBlur,
handleSuggestionsPopupMouseUp,
handleSuggestionsPopupSelectedEvent,
} = useSuggestionPopup(suggestionsPopupElement, lastCursorPosition, suggestionsData, null, emit, log, props);
} = useSuggestionPopup(
suggestionsPopupElement,
lastCursorPosition,
suggestionsData,
null,
emit,
log,
props,
);
watch(() => props.value, (newValue) => {
content.value = newValue;
});
watch(
() => props.value,
(newValue) => {
content.value = newValue;
},
);
const handleInput = () => {
emit("input", content.value);
emit('input', content.value);
};
const keyDown = (e) => {
@ -127,7 +140,7 @@ const pasteCanceled = async () => {
// set cursor to position after pasted string
await nextTick();
const field = inputField;
if (props.fieldType === "markdown") {
if (props.fieldType === 'markdown') {
field.editor.setCursor(lastCursorPosition.value);
field.editor.focus();
} else {
@ -145,13 +158,13 @@ const pasteCanceled = async () => {
const pasteString = async (str) => {
const field = inputField;
if (props.fieldType === "markdown") {
if (props.fieldType === 'markdown') {
// insert at last cursor position
field.editor.replaceRange(str, {
line: lastCursorPosition.value.line,
ch: lastCursorPosition.value.ch,
});
emit("input", content.value);
emit('input', content.value);
await nextTick();
await nextTick();
@ -166,7 +179,7 @@ const pasteString = async (str) => {
content.value.slice(0, lastCursorPosition.value) +
str +
content.value.slice(lastCursorPosition.value);
emit("input", content.value);
emit('input', content.value);
// set cursor to position after pasted string. Waiting two ticks is necessary for
// the data to be updated in the field
@ -178,13 +191,11 @@ const pasteString = async (str) => {
field.setSelectionRange(newCursorPos, newCursorPos);
}
};
</script>
<script>
export default {
name: 'EditInputField'
name: 'EditInputField',
};
</script>
@ -224,7 +235,7 @@ fieldset > div > input {
width: 100%;
}
fieldset input[type="number"] {
fieldset input[type='number'] {
width: 5em;
flex-grow: 0;
}
@ -246,7 +257,7 @@ Hack to overwrite the heavy-handed global unscoped styles of Nextcloud core
that cause our markdown editor CodeMirror to behave strangely on mobile
See: https://github.com/nextcloud/cookbook/issues/908
*/
.editor:deep(div[contenteditable="true"]) {
.editor:deep(div[contenteditable='true']) {
width: revert;
min-height: revert;
padding: revert;

Просмотреть файл

@ -69,20 +69,22 @@
ref="suggestionsPopupElement"
v-bind="suggestionsData"
:options="filteredSuggestionOptions"
v-on:suggestions-selected="handleSuggestionsPopupSelectedEvent"
v-on:suggestions-selected="
handleSuggestionsPopupSelectedEvent
"
/>
</li>
</ul>
<button class="button add-list-item pad-icon" @click="addNewEntry()">
<span class="icon-add"></span> {{ t("cookbook", "Add") }}
<span class="icon-add"></span> {{ t('cookbook', 'Add') }}
</button>
</fieldset>
</template>
<script setup>
import { getCurrentInstance, nextTick, ref, watch } from "vue";
import TriangleUpIcon from "icons/TriangleSmallUp.vue";
import TriangleDownIcon from "icons/TriangleSmallDown.vue";
import { getCurrentInstance, nextTick, ref, watch } from 'vue';
import TriangleUpIcon from 'icons/TriangleSmallUp.vue';
import TriangleDownIcon from 'icons/TriangleSmallDown.vue';
import SuggestionsPopup from '../Modals/SuggestionsPopup';
import useSuggestionPopup from '../../composables/useSuggestionsPopup';
@ -97,11 +99,11 @@ const props = defineProps({
},
fieldType: {
type: String,
default: "text",
default: 'text',
},
fieldName: {
type: String,
default: "",
default: '',
},
showStepNumber: {
type: Boolean,
@ -109,7 +111,7 @@ const props = defineProps({
},
fieldLabel: {
type: String,
default: "",
default: '',
},
// If true, add new fields, for newlines in pasted data
createFieldsOnNewlines: {
@ -117,7 +119,7 @@ const props = defineProps({
default: false,
},
suggestionOptions: {
type: Array
type: Array,
},
});
@ -145,9 +147,12 @@ const lastCursorPosition = ref(-1);
*/
const ignoreNextKeyUp = ref(false);
watch(() => props.value, (newValue) => {
buffer.value = newValue.slice();
});
watch(
() => props.value,
(newValue) => {
buffer.value = newValue.slice();
},
);
// deconstruct composable
let {
@ -166,14 +171,22 @@ let {
handleSuggestionsPopupBlur,
handleSuggestionsPopupMouseUp,
handleSuggestionsPopupSelectedEvent,
} = useSuggestionPopup(suggestionsPopupElement, lastCursorPosition, suggestionsData, buffer, emit, log, props);
} = useSuggestionPopup(
suggestionsPopupElement,
lastCursorPosition,
suggestionsData,
buffer,
emit,
log,
props,
);
watch(() => props.value,
watch(
() => props.value,
(val) => {
buffer.value = val.slice();
},
{ deep: true }
{ deep: true },
);
const linesMatchAtPosition = (lines, i) =>
@ -184,7 +197,7 @@ const findCommonPrefix = (lines) => {
// Inspired from https://stackoverflow.com/questions/68702774/longest-common-prefix-in-javascript
// Check border cases size 1 array and empty first word)
if (!lines[0] || lines.length === 1) return lines[0] || ""
if (!lines[0] || lines.length === 1) return lines[0] || '';
// Loop up index until the characters do not match
for (let i = 0; ; i++) {
@ -192,7 +205,7 @@ const findCommonPrefix = (lines) => {
// or the character of each line at position i is not identical
if (!lines[0][i] || !linesMatchAtPosition(lines, i)) {
// Then the desired prefix is the substring from the beginning to i
return lines[0].substr(0, i)
return lines[0].substr(0, i);
}
}
};
@ -201,12 +214,16 @@ const findCommonPrefix = (lines) => {
* if focusAfterInsert=true, the element is focussed after inserting
* the content is inserted into the newly created field
* */
const addNewEntry = async (index = -1, focusAfterInsert = true, content = "") => {
let entryIdx = index
const addNewEntry = async (
index = -1,
focusAfterInsert = true,
content = '',
) => {
let entryIdx = index;
if (entryIdx === -1) {
entryIdx = buffer.value.length
entryIdx = buffer.value.length;
}
buffer.value.splice(entryIdx, 0, content)
buffer.value.splice(entryIdx, 0, content);
if (focusAfterInsert) {
await nextTick();
@ -222,7 +239,7 @@ const addNewEntry = async (index = -1, focusAfterInsert = true, content = "") =>
*/
const deleteEntry = (index) => {
buffer.value.splice(index, 1);
emit("input", buffer.value);
emit('input', buffer.value);
};
/**
@ -234,12 +251,12 @@ const handleInput = (e) => {
// https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/inputType
// https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes
if (
e.inputType === "insertFromPaste" ||
e.inputType === "insertFromPasteAsQuotation"
e.inputType === 'insertFromPaste' ||
e.inputType === 'insertFromPasteAsQuotation'
) {
return;
}
emit("input", buffer.value);
emit('input', buffer.value);
};
/**
@ -249,16 +266,16 @@ const handlePaste = async (e) => {
// get data from clipboard to keep newline characters, which are stripped
// from the data pasted in the input field (e.target.value)
const clipboardData = e.clipboardData || window.clipboardData;
const pastedData = clipboardData.getData("Text");
const pastedData = clipboardData.getData('Text');
const inputLinesArray = pastedData
.split(/\r\n|\r|\n/g)
// Remove empty lines
.filter((line) => line.trim() !== "");
.filter((line) => line.trim() !== '');
// If only a single line pasted, emit that line and exit
// Treat it as if that single line was typed
if (inputLinesArray.length === 1) {
emit("input", buffer.value);
emit('input', buffer.value);
return;
}
@ -267,14 +284,11 @@ const handlePaste = async (e) => {
return;
}
e.preventDefault()
e.preventDefault();
const $li = e.currentTarget.closest("li");
const $ul = $li.closest("ul");
const $insertedIndex = Array.prototype.indexOf.call(
$ul.childNodes,
$li,
);
const $li = e.currentTarget.closest('li');
const $ul = $li.closest('ul');
const $insertedIndex = Array.prototype.indexOf.call($ul.childNodes, $li);
// Remove the common prefix from each line of the pasted text
// For example, if the pasted text uses - for a bullet list
@ -287,9 +301,7 @@ const handlePaste = async (e) => {
// as it should work for any alphabet
const re =
/[^\s\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~]/g;
const prefixLength = re.test(prefix)
? prefix.search(re)
: prefix.length;
const prefixLength = re.test(prefix) ? prefix.search(re) : prefix.length;
for (let i = 0; i < inputLinesArray.length; ++i) {
inputLinesArray[i] = inputLinesArray[i].slice(prefixLength);
@ -300,24 +312,18 @@ const handlePaste = async (e) => {
// to accidentally replace all newlines with spaces before splitting
// Fixes #713
for (let i = 0; i < inputLinesArray.length; ++i) {
inputLinesArray[i] = inputLinesArray[i]
.trim()
.replaceAll(/\s+/g, " ");
inputLinesArray[i] = inputLinesArray[i].trim().replaceAll(/\s+/g, ' ');
}
for (let i = 0; i < inputLinesArray.length; ++i) {
await addNewEntry(
$insertedIndex + i + 1,
false,
inputLinesArray[i],
);
await addNewEntry($insertedIndex + i + 1, false, inputLinesArray[i]);
}
emit("input", buffer.value)
emit('input', buffer.value);
await nextTick();
let indexToFocus = $insertedIndex + inputLinesArray.length
let indexToFocus = $insertedIndex + inputLinesArray.length;
// Delete field if it's empty
if (buffer.value[$insertedIndex].trim() === "") {
if (buffer.value[$insertedIndex].trim() === '') {
deleteEntry($insertedIndex);
indexToFocus -= 1;
}
@ -354,7 +360,7 @@ const keyDown = async (e) => {
}
// Only do anything for enter
if (e.key !== "Enter") {
if (e.key !== 'Enter') {
return;
}
@ -363,21 +369,18 @@ const keyDown = async (e) => {
e.preventDefault();
// Get the index of the pressed list item
const $li = e.currentTarget.closest("li");
const $ul = $li.closest("ul");
const $pressedLiIndex = Array.prototype.indexOf.call(
$ul.childNodes,
$li,
);
const $li = e.currentTarget.closest('li');
const $ul = $li.closest('ul');
const $pressedLiIndex = Array.prototype.indexOf.call($ul.childNodes, $li);
if ($pressedLiIndex >= this.$refs["list-field"].length - 1) {
if ($pressedLiIndex >= this.$refs['list-field'].length - 1) {
await addNewEntry();
} else {
// Focus the next input or textarea
// We have to check for both, as inputs are used for
// ingredients and textareas are used for instructions
$ul.children[$pressedLiIndex + 1]
.querySelector("input, textarea")
.querySelector('input, textarea')
.focus();
}
};
@ -393,15 +396,12 @@ const keyUp = (e) => {
return;
}
const $li = e.currentTarget.closest("li");
const $ul = $li.closest("ul");
const $li = e.currentTarget.closest('li');
const $ul = $li.closest('ul');
// noinspection UnnecessaryLocalVariableJS
const $pressedLiIndex = Array.prototype.indexOf.call(
$ul.childNodes,
$li,
);
lastFocusedFieldIndex.value = $pressedLiIndex
handleSuggestionsPopupKeyUp(e)
const $pressedLiIndex = Array.prototype.indexOf.call($ul.childNodes, $li);
lastFocusedFieldIndex.value = $pressedLiIndex;
handleSuggestionsPopupKeyUp(e);
};
const moveEntryDown = (index) => {
@ -415,7 +415,7 @@ const moveEntryDown = (index) => {
} else {
buffer.value.push(entry);
}
emit("input", buffer.value);
emit('input', buffer.value);
};
const moveEntryUp = (index) => {
@ -425,18 +425,15 @@ const moveEntryUp = (index) => {
}
const entry = buffer.value.splice(index, 1)[0];
buffer.value.splice(index - 1, 0, entry);
emit("input", buffer.value);
emit('input', buffer.value);
};
const pasteCanceled = async () => {
const field = listField[this.lastFocusedFieldIndex];
// set cursor back to previous position
await nextTick()
await nextTick();
field.focus();
field.setSelectionRange(
lastCursorPosition.value,
lastCursorPosition.value,
);
field.setSelectionRange(lastCursorPosition.value, lastCursorPosition.value);
};
/**
@ -453,7 +450,7 @@ const pasteString = async (str, ignoreKeyup = true) => {
str +
content.slice(lastCursorPosition.value);
buffer.value[lastFocusedFieldIndex.value] = updatedContent;
emit("input", buffer.value);
emit('input', buffer.value);
// set cursor to position after pasted string. Waiting two ticks is necessary for
// the data to be updated in the field
@ -468,7 +465,7 @@ const pasteString = async (str, ignoreKeyup = true) => {
<script>
export default {
name: 'EditInputGroup'
name: 'EditInputGroup',
};
</script>
@ -561,7 +558,7 @@ li .controls > button:last-child:not(:hover):not(:focus) {
.textarea::after {
display: table;
clear: both;
content: "";
content: '';
}
.step-number {

Просмотреть файл

@ -15,14 +15,14 @@ import NcMultiselect from '@nextcloud/vue/dist/Components/NcMultiselect';
defineProps({
fieldLabel: {
type: String,
default: "",
}
default: '',
},
});
</script>
<script>
export default {
name: 'EditMultiselect'
name: 'EditMultiselect',
};
</script>

Просмотреть файл

@ -12,7 +12,11 @@
:placeholder="labelSelectPlaceholder"
class="key"
/>
<input v-model="row.customText" :placeholder="row.options.placeholder" class="val" />
<input
v-model="row.customText"
:placeholder="row.options.placeholder"
class="val"
/>
</li>
</ul>
</fieldset>
@ -27,7 +31,7 @@ const emits = defineEmits(['input']);
const props = defineProps({
fieldLabel: {
type: String,
default: "",
default: '',
},
labelSelectPlaceholder: {
type: String,
@ -57,10 +61,13 @@ const props = defineProps({
*/
const rows = ref([]);
watch(() => props.value, async (newModelValue) => {
// React to external changes in modelValue
await createRowsBasedOnModelValue(newModelValue);
});
watch(
() => props.value,
async (newModelValue) => {
// React to external changes in modelValue
await createRowsBasedOnModelValue(newModelValue);
},
);
const createRowsBasedOnModelValue = async () => {
const initialModelValue = props.value || {};
@ -70,8 +77,8 @@ const createRowsBasedOnModelValue = async () => {
const option = props.options.find((opt) => opt.key === key);
const row = rows.value.find((row) => row.selectedOption.key === key);
// Update row with key if it already exists
if(row){
row.customText = initialModelValue[key] || ''
if (row) {
row.customText = initialModelValue[key] || '';
}
// otherwise create new row
else {
@ -87,12 +94,12 @@ const createRowsBasedOnModelValue = async () => {
await recalculateAvailableOptions();
await createRow(); // Create an additional row for future selections
}
};
const createRow = async () => {
// Remove empty rows at the end before creating a new one
for (let i = rows.value.length - 1; i >= 0; i--){
if(!(rows.value[i].selectedOption)){
for (let i = rows.value.length - 1; i >= 0; i--) {
if (!rows.value[i].selectedOption) {
rows.value.pop();
}
}
@ -106,7 +113,7 @@ const createRow = async () => {
customText: '',
});
}
}
};
const handleMultiselectChange = async (changedIndex) => {
// Wait for the DOM to update after the multiselect change
@ -122,20 +129,20 @@ const handleMultiselectChange = async (changedIndex) => {
emits('input', getSelectedValues());
await createRow();
}
};
const recalculateAvailableOptions = async () => {
// Update options in all rows based on the current selections
for (let i = 0; i < rows.value.length; i++) {
rows.value[i].options = await getAvailableOptions();
}
}
};
const getAvailableOptions = async () => {
// Calculate available options by excluding those already selected
const selectedOptions = rows.value.map((row) => row.selectedOption);
return props.options.filter((option) => !selectedOptions.includes(option));
}
};
const getSelectedValues = () => {
const selectedValues = {};
@ -146,13 +153,12 @@ const getSelectedValues = () => {
}
});
return selectedValues;
}
};
</script>
<script>
export default {
name: 'EditMultiselectInputGroup'
name: 'EditMultiselectInputGroup',
};
</script>

Просмотреть файл

@ -36,7 +36,7 @@ const props = defineProps({
},
fieldLabel: {
type: String,
default: "",
default: '',
},
});
@ -49,20 +49,23 @@ const hours = ref(null);
*/
const minutes = ref(null);
watch(() => props.value, () => {
hours.value = props.value.time[0];
minutes.value = props.value.time[1];
});
watch(
() => props.value,
() => {
hours.value = props.value.time[0];
minutes.value = props.value.time[1];
},
);
const handleInput = () => {
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 hoursPadded = hours.value.toString().padStart(2, '0');
const minutesPadded = minutes.value.toString().padStart(2, '0');
emit("input", {
emit('input', {
time: [hours.value, minutes.value],
paddedTime: `PT${hoursPadded}H${minutesPadded}M`,
});
@ -71,7 +74,7 @@ const handleInput = () => {
<script>
export default {
name: 'EditTimeField'
name: 'EditTimeField',
};
</script>

Просмотреть файл

@ -42,29 +42,29 @@
</template>
<script setup>
import moment from "@nextcloud/moment"
import LazyPicture from "../Utilities/LazyPicture.vue"
import moment from '@nextcloud/moment';
import LazyPicture from '../Utilities/LazyPicture.vue';
defineProps({
recipe: {
type: Object,
default: () => null,
}
},
});
const formatDateTime = (dt) => {
if (!dt) return null
const date = moment(dt, moment.ISO_8601)
if (!dt) return null;
const date = moment(dt, moment.ISO_8601);
if (!date.isValid()) {
return null
return null;
}
return date.format("L, LT").toString()
return date.format('L, LT').toString();
};
</script>
<script>
export default {
name: 'RecipeCard'
name: 'RecipeCard',
};
</script>

Просмотреть файл

@ -115,7 +115,7 @@
</template>
<script setup>
import { computed,onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router/composables';
import { useStore } from '../../store';
import RecipeIcon from 'vue-material-design-icons/ChefHat.vue';
@ -190,7 +190,7 @@ const orderBy = ref(recipeOrderingOptions.value[0]);
onMounted(() => {
store.dispatch('clearRecipeFilters');
})
});
// Computed properties
/**
@ -252,7 +252,9 @@ const filteredRecipes = computed(() => {
let ret = recipesFilteredByKeywords.value;
if (!!store.state.recipeFilters) {
ret = ret.filter((r) =>
r.name.toLowerCase().includes(store.state.recipeFilters.toLowerCase()),
r.name
.toLowerCase()
.includes(store.state.recipeFilters.toLowerCase()),
);
}
return ret;
@ -314,7 +316,7 @@ const recipeObjects = computed(() => {
}
if (orderBy.value.recipeProperty === 'dateModified') {
if (orderBy.value.order === 'ascending') {
return recipesDateModifiedAsc.value.map(makeObject)
return recipesDateModifiedAsc.value.map(makeObject);
}
return recipesDateModifiedDesc.value.map(makeObject);
}
@ -346,18 +348,13 @@ const sortRecipes = (recipes, recipeProperty, order) => {
recipeProperty === 'dateModified'
) {
return (
new Date(r1[recipeProperty]) -
new Date(r2[recipeProperty])
new Date(r1[recipeProperty]) - new Date(r2[recipeProperty])
);
}
if (recipeProperty === 'name') {
return r1[recipeProperty].localeCompare(
r2[recipeProperty],
);
return r1[recipeProperty].localeCompare(r2[recipeProperty]);
}
if (
!Number.isNaN(r1[recipeProperty] - r2[recipeProperty])
) {
if (!Number.isNaN(r1[recipeProperty] - r2[recipeProperty])) {
return r1[recipeProperty] - r2[recipeProperty];
}
return 0;
@ -367,10 +364,7 @@ const sortRecipes = (recipes, recipeProperty, order) => {
recipeProperty === 'dateCreated' ||
recipeProperty === 'dateModified'
) {
return (
new Date(r2[recipeProperty]) -
new Date(r1[recipeProperty])
);
return new Date(r2[recipeProperty]) - new Date(r1[recipeProperty]);
}
if (recipeProperty === 'name') {
return r2[recipeProperty].localeCompare(r1[recipeProperty]);
@ -385,7 +379,7 @@ const sortRecipes = (recipes, recipeProperty, order) => {
<script>
export default {
name: 'RecipeList'
name: 'RecipeList',
};
</script>

Просмотреть файл

@ -130,9 +130,7 @@ const keywordsWithCount = computed(() => {
}))
.sort((k1, k2) => {
if (isOrderedAlphabetically.value) {
return k1.name.toLowerCase() > k2.name.toLowerCase()
? 1
: -1;
return k1.name.toLowerCase() > k2.name.toLowerCase() ? 1 : -1;
}
// else: order by number of recipe with this keyword (decreasing)
if (k1.count !== k2.count) {
@ -140,9 +138,7 @@ const keywordsWithCount = computed(() => {
return k2.count - k1.count;
}
// Distinguish by keyword name
return k1.name.toLowerCase() > k2.name.toLowerCase()
? 1
: -1;
return k1.name.toLowerCase() > k2.name.toLowerCase() ? 1 : -1;
});
});
/**
@ -155,11 +151,7 @@ const selectableKeywords = computed(() => {
return unselectedKeywords.value.filter((kw) =>
props.filteredRecipes
.map(
(r) =>
r.keywords &&
r.keywords.split(',').includes(kw.name),
)
.map((r) => r.keywords && r.keywords.split(',').includes(kw.name))
.reduce((l, r) => l || r, false),
);
});
@ -192,11 +184,12 @@ const unselectedKeywords = computed(() => {
/**
* Watch array of selected keywords for changes
*/
watch(() => props.value,
watch(
() => props.value,
() => {
selectedKeywordsBuffer.value = props.value.slice()
selectedKeywordsBuffer.value = props.value.slice();
},
{ deep: true }
{ deep: true },
);
// Methods
@ -204,7 +197,7 @@ watch(() => props.value,
* Callback for click on keyword, add to or remove from list
*/
const keywordClicked = (keyword) => {
const index = selectedKeywordsBuffer.value.indexOf(keyword.name)
const index = selectedKeywordsBuffer.value.indexOf(keyword.name);
if (index > -1) {
selectedKeywordsBuffer.value.splice(index, 1);
} else {
@ -222,7 +215,7 @@ const toggleOrderCriterion = () => {
<script>
export default {
name: 'RecipeListKeywordCloud'
name: 'RecipeListKeywordCloud',
};
</script>

Просмотреть файл

@ -173,10 +173,14 @@
</NcAppSettingsDialog>
</template>
<script setup>
import { getCurrentInstance, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import {
getCurrentInstance,
onBeforeUnmount,
onMounted,
ref,
watch,
} from 'vue';
import { subscribe, unsubscribe } from '@nextcloud/event-bus';
import { getFilePickerBuilder } from '@nextcloud/dialogs';
@ -264,83 +268,94 @@ const visibleInfoBlocks = ref([...INFO_BLOCK_KEYS]);
const resetVisibleInfoBlocks = ref(true);
// Watchers
watch(() => printImage.value, async (newVal, oldVal) => {
// Avoid infinite loop on page load and when resetting value after failed submit
if (resetPrintImage.value) {
resetPrintImage.value = false;
return;
}
try {
await api.config.printImage.update(newVal);
// Should this check the response of the query? To catch some errors that redirect the page
} catch {
await showSimpleAlertModal(
// prettier-ignore
t('cookbook','Could not set preference for image printing'),
);
resetPrintImage.value = true;
printImage.value = oldVal;
}
});
watch(
() => printImage.value,
async (newVal, oldVal) => {
// Avoid infinite loop on page load and when resetting value after failed submit
if (resetPrintImage.value) {
resetPrintImage.value = false;
return;
}
try {
await api.config.printImage.update(newVal);
// Should this check the response of the query? To catch some errors that redirect the page
} catch {
await showSimpleAlertModal(
// prettier-ignore
t('cookbook','Could not set preference for image printing'),
);
resetPrintImage.value = true;
printImage.value = oldVal;
}
},
);
// eslint-disable-next-line no-unused-vars
watch(() => showTagCloudInRecipeList.value, (newVal) => {
store.dispatch('setShowTagCloudInRecipeList', {
showTagCloud: newVal,
});
});
watch(
() => showTagCloudInRecipeList.value,
(newVal) => {
store.dispatch('setShowTagCloudInRecipeList', {
showTagCloud: newVal,
});
},
);
watch(() => updateInterval.value, async (newVal, oldVal) => {
// Avoid infinite loop on page load and when resetting value after failed submit
if (resetInterval.value) {
resetInterval.value = false;
return;
}
try {
await api.config.updateInterval.update(newVal);
// Should this check the response of the query? To catch some errors that redirect the page
} catch {
await showSimpleAlertModal(
// prettier-ignore
t('cookbook','Could not set recipe update interval to {interval}',
watch(
() => updateInterval.value,
async (newVal, oldVal) => {
// Avoid infinite loop on page load and when resetting value after failed submit
if (resetInterval.value) {
resetInterval.value = false;
return;
}
try {
await api.config.updateInterval.update(newVal);
// Should this check the response of the query? To catch some errors that redirect the page
} catch {
await showSimpleAlertModal(
// prettier-ignore
t('cookbook','Could not set recipe update interval to {interval}',
{
interval: newVal,
}
),
);
resetInterval.value = true;
updateInterval.value = oldVal;
}
});
);
resetInterval.value = true;
updateInterval.value = oldVal;
}
},
);
watch(() => visibleInfoBlocks.value, async (newVal, oldVal) => {
// Avoid infinite loop on page load and when resetting value after failed submit
if (resetVisibleInfoBlocks.value) {
resetVisibleInfoBlocks.value = false;
return;
}
try {
const data = visibleInfoBlocksEncode(newVal);
await api.config.visibleInfoBlocks.update(data);
await store.dispatch('refreshConfig');
// Should this check the response of the query? To catch some errors that redirect the page
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error while trying to save info blocks', err);
await showSimpleAlertModal(
t('cookbook', 'Could not save visible info blocks'),
);
resetVisibleInfoBlocks.value = true;
visibleInfoBlocks.value = oldVal;
}
});
watch(
() => visibleInfoBlocks.value,
async (newVal, oldVal) => {
// Avoid infinite loop on page load and when resetting value after failed submit
if (resetVisibleInfoBlocks.value) {
resetVisibleInfoBlocks.value = false;
return;
}
try {
const data = visibleInfoBlocksEncode(newVal);
await api.config.visibleInfoBlocks.update(data);
await store.dispatch('refreshConfig');
// Should this check the response of the query? To catch some errors that redirect the page
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error while trying to save info blocks', err);
await showSimpleAlertModal(
t('cookbook', 'Could not save visible info blocks'),
);
resetVisibleInfoBlocks.value = true;
visibleInfoBlocks.value = oldVal;
}
},
);
onMounted(() => {
setup();
subscribe(SHOW_SETTINGS_EVENT, handleShowSettings);
});
onBeforeUnmount(() => {
unsubscribe(SHOW_SETTINGS_EVENT, handleShowSettings);
});
@ -381,7 +396,7 @@ const pickRecipeFolder = () => {
),
),
);
})
});
};
/**
@ -408,9 +423,7 @@ const setup = async () => {
recipeFolder.value = config.folder;
} catch (err) {
log.error('Error setting up SettingsDialog', err);
await showSimpleAlertModal(
t('cookbook', 'Loading config failed'),
);
await showSimpleAlertModal(t('cookbook', 'Loading config failed'));
}
};
@ -428,9 +441,7 @@ const reindex = () => {
.then(() => {
scanningLibrary.value = false;
log.info('Library reindexing complete');
if (
['index', 'search'].indexOf(store.state.page) > -1
) {
if (['index', 'search'].indexOf(store.state.page) > -1) {
// This refreshes the current router view in case items in it changed during reindex
router.go();
} else {
@ -444,7 +455,7 @@ const reindex = () => {
};
const enableLogger = () => {
enableLogging()
enableLogging();
};
</script>
@ -461,8 +472,8 @@ export default {
</style>
<style>
#app-settings input[type="text"],
#app-settings input[type="number"],
#app-settings input[type='text'],
#app-settings input[type='number'],
#app-settings .button.disable {
display: block;
width: 100%;

Просмотреть файл

@ -28,7 +28,7 @@
</template>
<script setup>
import { computed, defineProps, onMounted, ref, watch } from "vue";
import { computed, defineProps, onMounted, ref, watch } from 'vue';
const SUGGESTIONS_POPUP_WIDTH = 300;
const emit = defineEmits(['suggestions-selected']);
@ -72,9 +72,7 @@ const offset = computed(() => {
return {
left: Math.min(
props.field.offsetLeft + caretPos.left,
field.offsetLeft +
field.offsetWidth -
SUGGESTIONS_POPUP_WIDTH
field.offsetLeft + field.offsetWidth - SUGGESTIONS_POPUP_WIDTH,
),
top: field.offsetTop + caretPos.top + caretPos.height,
};
@ -84,22 +82,25 @@ const offset = computed(() => {
* Scroll to centre the focused element in the parent when it changes
* (with arrow keys, for example)
*/
watch(() => props.focusIndex, (focusIndex) => {
if(scroller.value === null) return;
watch(
() => props.focusIndex,
(focusIndex) => {
if (scroller.value === null) return;
const parentHeight = scroller.value.offsetHeight;
const childHeight = scroller.value.children[0].offsetHeight;
const parentHeight = scroller.value.offsetHeight;
const childHeight = scroller.value.children[0].offsetHeight;
// Get the scroll position of the top of the focused element
const focusedChildTop = childHeight * focusIndex;
// Get the centre
const focusedChildMiddle = focusedChildTop + childHeight / 2;
// Offset to centre in the parent scrolling element
const parentMiddle = focusedChildMiddle - parentHeight / 2;
// Get the scroll position of the top of the focused element
const focusedChildTop = childHeight * focusIndex;
// Get the centre
const focusedChildMiddle = focusedChildTop + childHeight / 2;
// Offset to centre in the parent scrolling element
const parentMiddle = focusedChildMiddle - parentHeight / 2;
// Scroll to that position
scroller.value.scrollTo(0, parentMiddle);
});
// Scroll to that position
scroller.value.scrollTo(0, parentMiddle);
},
);
onMounted(() => {
scroller.value.scrollTo(0, 0);
@ -118,7 +119,7 @@ const handleClick = (e) => {
<script>
export default {
name: 'SuggestionsPopup'
name: 'SuggestionsPopup',
};
</script>

Просмотреть файл

@ -1,5 +1,5 @@
<template>
<h2>{{ t("cookbook", "The page was not found") }}</h2>
<h2>{{ t('cookbook', 'The page was not found') }}</h2>
</template>
<script setup>
@ -9,13 +9,13 @@ import { useStore } from '../store';
const store = useStore();
onMounted(() => {
store.dispatch("setPage", { page: "notfound" });
store.dispatch('setPage', { page: 'notfound' });
});
</script>
<script>
export default {
name: 'NotFound'
name: 'NotFound',
};
</script>

Просмотреть файл

@ -113,16 +113,28 @@
: 'icon-checkmark'
"
></span>
{{ t("cookbook", "Save") }}
{{ t('cookbook', 'Save') }}
</button>
</div>
</div>
</template>
<script setup>
import {computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue';
import {
computed,
getCurrentInstance,
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch,
} from 'vue';
import {onBeforeRouteLeave, onBeforeRouteUpdate, useRoute} from 'vue-router/composables';
import {
onBeforeRouteLeave,
onBeforeRouteUpdate,
useRoute,
} from 'vue-router/composables';
import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
@ -320,14 +332,13 @@ const overlayVisible = computed(() => {
store.state.loadingRecipe ||
store.state.reloadingRecipe ||
(store.state.categoryUpdating &&
store.state.categoryUpdating ===
recipe.value.recipeCategory)
store.state.categoryUpdating === recipe.value.recipeCategory)
);
});
const recipeWithCorrectedYield = computed(() => {
const r = recipe.value;
if (!showRecipeYield.value) {
r.recipeYield = null
r.recipeYield = null;
}
return r;
});
@ -337,41 +348,45 @@ const isNavigationDangerous = computed(() => {
return !savingRecipe.value && formDirty.value;
});
// ===================
// Watchers
// ===================
watch(() => prepTime.value,
watch(
() => prepTime.value,
() => {
recipe.value.prepTime = prepTime.value.paddedTime;
},
{ deep: true }
{ deep: true },
);
watch(() => cookTime.value,
watch(
() => cookTime.value,
() => {
recipe.value.cookTime = cookTime.value.paddedTime;
},
{ deep: true }
{ deep: true },
);
watch(() => totalTime.value,
watch(
() => totalTime.value,
() => {
recipe.value.totalTime = totalTime.value.paddedTime;
},
{ deep: true }
{ deep: true },
);
watch(() => selectedKeywords.value,
watch(
() => selectedKeywords.value,
() => {
// convert keyword array to comma-separated string
recipe.value.keywords = selectedKeywords.value.join();
},
{ deep: true }
{ deep: true },
);
watch(() => recipe.value,
watch(
() => recipe.value,
() => {
formDirty.value = true
formDirty.value = true;
},
{ deep: true }
{ deep: true },
);
// ===================
@ -436,7 +451,7 @@ onMounted(() => {
// Register data load method hook for access from the controls components
emitter.off('reloadRecipeEdit');
emitter.on('reloadRecipeEdit', () => {
loadRecipeData()
loadRecipeData();
});
emitter.off('categoryRenamed');
emitter.on('categoryRenamed', (val) => {
@ -465,14 +480,13 @@ onMounted(() => {
.then(() => {
// finally
loadingRecipeReferences.value = false;
})
});
});
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', beforeWindowUnload);
});
// ===================
// Methods
// ===================
@ -521,9 +535,7 @@ const fetchCategories = async () => {
}
isFetchingCategories.value = false;
} catch (e) {
await showSimpleAlertModal(
t('cookbook', 'Failed to fetch categories'),
);
await showSimpleAlertModal(t('cookbook', 'Failed to fetch categories'));
if (e && e instanceof Error) {
throw e;
}
@ -546,9 +558,7 @@ const fetchKeywords = async () => {
}
isFetchingKeywords.value = false;
} catch (e) {
await showSimpleAlertModal(
t('cookbook', 'Failed to fetch keywords'),
);
await showSimpleAlertModal(t('cookbook', 'Failed to fetch keywords'));
if (e && e instanceof Error) {
throw e;
}
@ -560,10 +570,7 @@ const loadRecipeData = async () => {
store.dispatch('setLoadingRecipe', {
recipe: -1,
});
} else if (
store.state.recipe.id ===
parseInt(route.params.id, 10)
) {
} else if (store.state.recipe.id === parseInt(route.params.id, 10)) {
// Make the control row show that the recipe is reloading
store.dispatch('setReloadingRecipe', {
recipe: route.params.id,
@ -575,12 +582,10 @@ const loadRecipeData = async () => {
store.dispatch('setRecipe', { recipe });
setup();
} catch {
await showSimpleAlertModal(
t('cookbook', 'Loading recipe failed'),
);
await showSimpleAlertModal(t('cookbook', 'Loading recipe failed'));
// Disable loading indicator
if (store.state.loadingRecipe) {
store.dispatch('setLoadingRecipe', { recipe: 0 })
store.dispatch('setLoadingRecipe', { recipe: 0 });
} else if ($store.state.reloadingRecipe) {
store.dispatch('setReloadingRecipe', {
recipe: 0,
@ -643,7 +648,7 @@ const save = async () => {
savingRecipe.value = false;
}
};
const setup = async () =>{
const setup = async () => {
fetchCategories();
fetchKeywords();
if (route.params.id) {
@ -687,7 +692,7 @@ const setup = async () =>{
// fallback if fetching all keywords fails
selectedKeywords.value.forEach((kw) => {
if (!allKeywords.value.includes(kw)) {
allKeywords.value.push(kw)
allKeywords.value.push(kw);
}
});
@ -752,7 +757,6 @@ loadRecipeData();
</script>
<script>
export default {
// We can check if the user has browsed from the same recipe's view to this
// edit and save some time by not reloading the recipe data, leading to a

Просмотреть файл

@ -32,12 +32,12 @@ const clicked = () => {
if (!link.value.classList.contains('disabled')) {
emit('keyword-clicked');
}
}
};
</script>
<script>
export default {
name: 'RecipeKeyword'
name: 'RecipeKeyword',
};
</script>

Просмотреть файл

@ -15,7 +15,7 @@
</template>
<script setup>
import { ref } from "vue";
import { ref } from 'vue';
/**
* @type {import('vue').Ref<boolean>}
@ -23,13 +23,13 @@ import { ref } from "vue";
const collapsed = ref(true);
const toggleCollapsed = () => {
this.collapsed = !this.collapsed
this.collapsed = !this.collapsed;
};
</script>
<script>
export default {
name: 'RecipeImages'
name: 'RecipeImages',
};
</script>

Просмотреть файл

@ -17,14 +17,14 @@
<script setup>
import { computed, ref } from 'vue';
const headerPrefix = "## ";
const headerPrefix = '## ';
const props = defineProps({
/* Ingredient HTML string to display. Content should be sanitized.
*/
ingredient: {
type: String,
default: "",
default: '',
},
ingredientHasCorrectSyntax: {
type: Boolean,
@ -57,7 +57,7 @@ const toggleDone = () => {
<script>
export default {
name: 'RecipeIngredient'
name: 'RecipeIngredient',
};
</script>

Просмотреть файл

@ -12,7 +12,7 @@ defineProps({
*/
instruction: {
type: String,
default: "",
default: '',
},
});
@ -28,7 +28,7 @@ const toggleDone = () => {
<script>
export default {
name: 'RecipeInstruction'
name: 'RecipeInstruction',
};
</script>
@ -65,11 +65,11 @@ li:hover::before {
}
.done::before {
content: "✔";
content: '✔';
}
li span,
li input[type="checkbox"] {
li input[type='checkbox'] {
display: inline-block;
width: 1rem;
height: auto;

Просмотреть файл

@ -24,7 +24,7 @@ defineProps({
<script>
export default {
name: 'RecipeNutritionInfoItem'
name: 'RecipeNutritionInfoItem',
};
</script>

Просмотреть файл

@ -21,7 +21,7 @@ const props = defineProps({
value: {
type: Object,
default() {
return { hours: 0, minutes: 0 }
return { hours: 0, minutes: 0 };
},
},
label: {
@ -71,9 +71,12 @@ const displayTime = computed(() => {
});
// Watchers
watch(() => props.value, () => {
resetTimeDisplay();
});
watch(
() => props.value,
() => {
resetTimeDisplay();
},
);
onMounted(() => {
resetTimeDisplay();
@ -82,9 +85,7 @@ onMounted(() => {
// Source for the sound https://pixabay.com/sound-effects/alarm-clock-short-6402/
// Voted by poll https://nextcloud.christian-wolf.click/nextcloud/apps/polls/s/Wke3s6CscDwQEjPV
audio.value = new Audio(
linkTo('cookbook', 'assets/alarm-continuous.mp3'),
);
audio.value = new Audio(linkTo('cookbook', 'assets/alarm-continuous.mp3'));
// For now, the alarm should play continuously until it is dismissed
// See https://github.com/nextcloud/cookbook/issues/671#issuecomment-1279030452
@ -164,7 +165,7 @@ const timerToggle = () => {
<script>
export default {
name: 'RecipeTimer'
name: 'RecipeTimer',
};
</script>

Просмотреть файл

@ -15,7 +15,7 @@ defineProps({
<script>
export default {
name: 'RecipeTool'
name: 'RecipeTool',
};
</script>

Просмотреть файл

@ -55,7 +55,7 @@
class="markdown-description"
/>
<p v-if="$store.state.recipe.url">
<strong>{{ t("cookbook", "Source") }}: </strong
<strong>{{ t('cookbook', 'Source') }}: </strong
><a
target="_blank"
:href="$store.state.recipe.url"
@ -65,7 +65,7 @@
</p>
<div>
<p v-if="$store.state.recipe.recipeYield != null">
<strong>{{ t("cookbook", "Servings") }}: </strong>
<strong>{{ t('cookbook', 'Servings') }}: </strong>
<span>
<button
:disabled="recipeYield === 1"
@ -130,7 +130,7 @@
<section class="container">
<section class="ingredients">
<h3 v-if="scaledIngredients.length" class="section-title">
<span>{{ t("cookbook", "Ingredients") }}</span>
<span>{{ t('cookbook', 'Ingredients') }}</span>
<NcButton
class="copy-ingredients"
:type="'tertiary'"
@ -178,7 +178,7 @@
<section v-if="visibleInfoBlocks.tools" class="tools">
<h3 v-if="parsedTools.length">
{{ t("cookbook", "Tools") }}
{{ t('cookbook', 'Tools') }}
</h3>
<ul v-if="parsedTools.length">
<RecipeTool
@ -190,7 +190,7 @@
</section>
<section v-if="showNutritionData" class="nutrition">
<h3>{{ t("cookbook", "Nutrition Information") }}</h3>
<h3>{{ t('cookbook', 'Nutrition Information') }}</h3>
<ul class="nutrition-items">
<recipe-nutrition-info-item
v-if="
@ -306,7 +306,7 @@
</section>
<main v-if="parsedInstructions.length">
<h3>{{ t("cookbook", "Instructions") }}</h3>
<h3>{{ t('cookbook', 'Instructions') }}</h3>
<ol class="instructions">
<RecipeInstruction
v-for="(instruction, idx) in parsedInstructions"
@ -322,7 +322,11 @@
<script setup>
import { computed, getCurrentInstance, onMounted, ref, watch } from 'vue';
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router/composables';
import {
onBeforeRouteUpdate,
useRoute,
useRouter,
} from 'vue-router/composables';
import api from 'cookbook/js/api-interface';
import helpers from 'cookbook/js/helper';
@ -330,7 +334,7 @@ import normalizeMarkdown from 'cookbook/js/title-rename';
import { showSimpleAlertModal } from 'cookbook/js/modals';
import { useStore } from '../../store';
import emitter from '../../bus';
import { parseDateTime } from "../../composables/dateTimeHandling";
import { parseDateTime } from '../../composables/dateTimeHandling';
import yieldCalculator from 'cookbook/js/yieldCalculator';
import ContentCopyIcon from 'icons/ContentCopy.vue';
@ -350,7 +354,6 @@ const router = useRouter();
const store = useStore();
const log = getCurrentInstance().proxy.$log;
/**
* This is one tricky feature of Vue router. If different paths lead to
* the same component (such as '/recipe/xxx' and '/recipe/yyy)',
@ -426,9 +429,7 @@ const recipe = computed(() => {
}
if (store.state.recipe.description) {
recipe.description = helpers.escapeHTML(
store.state.recipe.description,
);
recipe.description = helpers.escapeHTML(store.state.recipe.description);
}
if (store.state.recipe.recipeIngredient) {
@ -444,14 +445,11 @@ const recipe = computed(() => {
}
if (store.state.recipe.keywords) {
recipe.keywords = String(
store.state.recipe.keywords,
).split(',');
recipe.keywords = String(store.state.recipe.keywords).split(',');
}
if (store.state.recipe.cookTime) {
const cookT =
store.state.recipe.cookTime.match(/PT(\d+?)H(\d+?)M/);
const cookT = store.state.recipe.cookTime.match(/PT(\d+?)H(\d+?)M/);
const hh = parseInt(cookT[1], 10);
const mm = parseInt(cookT[2], 10);
if (hh > 0 || mm > 0) {
@ -460,8 +458,7 @@ const recipe = computed(() => {
}
if (store.state.recipe.prepTime) {
const prepT =
store.state.recipe.prepTime.match(/PT(\d+?)H(\d+?)M/);
const prepT = store.state.recipe.prepTime.match(/PT(\d+?)H(\d+?)M/);
const hh = parseInt(prepT[1], 10);
const mm = parseInt(prepT[2], 10);
if (hh > 0 || mm > 0) {
@ -470,8 +467,7 @@ const recipe = computed(() => {
}
if (store.state.recipe.totalTime) {
const totalT =
store.state.recipe.totalTime.match(/PT(\d+?)H(\d+?)M/);
const totalT = store.state.recipe.totalTime.match(/PT(\d+?)H(\d+?)M/);
const hh = parseInt(totalT[1], 10);
const mm = parseInt(totalT[2], 10);
if (hh > 0 || mm > 0) {
@ -486,17 +482,13 @@ const recipe = computed(() => {
}
if (store.state.recipe.dateCreated) {
const date = parseDateTime(
store.state.recipe.dateCreated,
);
const date = parseDateTime(store.state.recipe.dateCreated);
recipe.dateCreated =
date != null ? date.format('L, LT').toString() : null;
}
if (store.state.recipe.dateModified) {
const date = parseDateTime(
store.state.recipe.dateModified,
);
const date = parseDateTime(store.state.recipe.dateModified);
recipe.dateModified =
date != null ? date.format('L, LT').toString() : null;
}
@ -517,11 +509,7 @@ const recipe = computed(() => {
const recipeIngredientsHaveSubgroups = computed(() => {
if (recipe.value.ingredients && recipe.value.ingredients.length > 0) {
for (let idx = 0; idx < recipe.value.ingredients.length; ++idx) {
if (
recipe.value.ingredients[idx].startsWith(
headerPrefix,
)
) {
if (recipe.value.ingredients[idx].startsWith(headerPrefix)) {
return true;
}
}
@ -540,8 +528,7 @@ const showModifiedDate = computed(() => {
return !(
store.state.recipe.dateCreated &&
store.state.recipe.dateModified &&
store.state.recipe.dateCreated ===
store.state.recipe.dateModified
store.state.recipe.dateCreated === store.state.recipe.dateModified
);
});
@ -567,9 +554,7 @@ const scaledIngredients = computed(() => {
});
const ingredientsWithValidSyntax = computed(() => {
return parsedIngredients.value.map(
yieldCalculator.isValidIngredientSyntax,
);
return parsedIngredients.value.map(yieldCalculator.isValidIngredientSyntax);
});
const ingredientsSyntaxCorrect = computed(() => {
@ -577,82 +562,88 @@ const ingredientsSyntaxCorrect = computed(() => {
});
// Watchers
watch(() => recipe.value, (r) => {
log.debug('Recipe has been updated');
if (r) {
log.debug('Recipe', r);
watch(
() => recipe.value,
(r) => {
log.debug('Recipe has been updated');
if (r) {
log.debug('Recipe', r);
if (r.description) {
parsedDescription.value = t('cookbook', 'Loading…');
normalizeMarkdown(r.description).then((x) => {
parsedDescription.value = x;
});
} else {
parsedDescription.value = '';
if (r.description) {
parsedDescription.value = t('cookbook', 'Loading…');
normalizeMarkdown(r.description).then((x) => {
parsedDescription.value = x;
});
} else {
parsedDescription.value = '';
}
if (r.ingredients) {
parsedIngredients.value = r.ingredients.map(() =>
t('cookbook', 'Loading…'),
);
r.ingredients.forEach((ingredient, idx) => {
normalizeMarkdown(ingredient)
.then((x) => {
parsedIngredients.value.splice(idx, 1, x);
})
.catch((ex) => {
log.error(ex);
});
});
} else {
parsedIngredients.value = [];
}
if (r.instructions) {
parsedInstructions.value = r.instructions.map(() =>
t('cookbook', 'Loading…'),
);
r.instructions.forEach((instruction, idx) => {
normalizeMarkdown(instruction)
.then((x) => {
parsedInstructions.value.splice(idx, 1, x);
})
.catch((ex) => {
log.error(ex);
});
});
} else {
parsedInstructions.value = [];
}
if (r.tools) {
parsedTools.value = r.tools.map(() =>
t('cookbook', 'Loading…'),
);
r.tools.forEach((tool, idx) => {
normalizeMarkdown(tool)
.then((x) => {
parsedTools.value.splice(idx, 1, x);
})
.catch((ex) => {
log.error(ex);
});
});
} else {
parsedTools.value = [];
}
}
},
);
if (r.ingredients) {
parsedIngredients.value = r.ingredients.map(() =>
t('cookbook', 'Loading…'),
);
r.ingredients.forEach((ingredient, idx) => {
normalizeMarkdown(ingredient)
.then((x) => {
parsedIngredients.value.splice(idx, 1, x);
})
.catch((ex) => {
log.error(ex);
});
});
} else {
parsedIngredients.value = [];
watch(
() => recipeYield.value,
() => {
if (recipeYield.value < 0) {
restoreOriginalRecipeYield();
}
if (r.instructions) {
parsedInstructions.value = r.instructions.map(() =>
t('cookbook', 'Loading…'),
);
r.instructions.forEach((instruction, idx) => {
normalizeMarkdown(instruction)
.then((x) => {
parsedInstructions.value.splice(idx, 1, x);
})
.catch((ex) => {
log.error(ex);
})
});
} else {
parsedInstructions.value = [];
}
if (r.tools) {
parsedTools.value = r.tools.map(() =>
t('cookbook', 'Loading…'),
);
r.tools.forEach((tool, idx) => {
normalizeMarkdown(tool)
.then((x) => {
parsedTools.value.splice(idx, 1, x);
})
.catch((ex) => {
log.error(ex);
})
});
} else {
parsedTools.value = [];
}
}
});
watch(() => recipeYield.value, () => {
if (recipeYield.value < 0) {
restoreOriginalRecipeYield();
}
});
},
);
// Methods
const isNullOrEmpty = (str) => {
return !str || (typeof str === 'string' && str.trim().length === 0)
return !str || (typeof str === 'string' && str.trim().length === 0);
};
/**
@ -670,10 +661,7 @@ const setup = async () => {
store.dispatch('setLoadingRecipe', { recipe: -1 });
// Make the control row show that the recipe is reloading
} else if (
store.state.recipe.id ===
parseInt(route.params.id, 10)
) {
} else if (store.state.recipe.id === parseInt(route.params.id, 10)) {
store.dispatch('setReloadingRecipe', {
recipe: route.params.id,
});
@ -708,16 +696,14 @@ const setup = async () => {
store.dispatch('setPage', { page: 'recipe' });
await showSimpleAlertModal(
t('cookbook', 'Loading recipe failed'),
);
await showSimpleAlertModal(t('cookbook', 'Loading recipe failed'));
}
recipeYield.value = store.state.recipe.recipeYield;
};
const changeRecipeYield = (increase = true) => {
recipeYield.value = recipeYield.value + (increase ? 1 : -1)
recipeYield.value = recipeYield.value + (increase ? 1 : -1);
};
const copyIngredientsToClipboard = () => {
@ -726,12 +712,8 @@ const copyIngredientsToClipboard = () => {
if (navigator.clipboard) {
navigator.clipboard
.writeText(ingredientsToCopy)
.then(() =>
log.info('JSON array copied to clipboard'),
)
.catch((err) =>
log.error('Failed to copy JSON array: ', err),
);
.then(() => log.info('JSON array copied to clipboard'))
.catch((err) => log.error('Failed to copy JSON array: ', err));
} else {
// fallback solution
const input = document.createElement('textarea');
@ -762,7 +744,7 @@ const restoreOriginalRecipeYield = () => {
<script>
export default {
name: 'RecipeView'
name: 'RecipeView',
};
</script>
@ -786,7 +768,7 @@ export default {
}
.header a::after {
content: "";
content: '';
}
}
@ -899,7 +881,7 @@ section {
section::after {
display: table;
clear: both;
content: "";
content: '';
}
.content {
@ -935,7 +917,7 @@ aside ul li {
}
aside ul li span,
aside ul li input[type="checkbox"] {
aside ul li input[type='checkbox'] {
display: inline-block;
width: 1rem;
height: auto;
@ -1021,7 +1003,7 @@ main {
}
.instructions .instruction.done::before {
content: "✔";
content: '✔';
}
.content > .container {

Просмотреть файл

@ -6,7 +6,7 @@
<script setup>
import { onActivated, onDeactivated, onMounted, ref } from 'vue';
import { onBeforeRouteUpdate, useRoute } from "vue-router/composables";
import { onBeforeRouteUpdate, useRoute } from 'vue-router/composables';
import api from 'cookbook/js/api-interface';
import helpers from 'cookbook/js/helper';
import { showSimpleAlertModal } from 'cookbook/js/modals';
@ -119,9 +119,7 @@ const setup = async () => {
} else {
// General search
try {
const response = await api.recipes.search(
route.params.value,
);
const response = await api.recipes.search(route.params.value);
results.value = response.data;
} catch (e) {
results.value = [];

Просмотреть файл

@ -25,7 +25,7 @@
</template>
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import lozad from 'lozad';
const props = defineProps({
@ -65,7 +65,7 @@ const fullImage = ref(null);
const previewImage = ref(null);
const style = computed(() => {
const style = {}
const style = {};
if (props.width) {
style.width = `${props.width}px`;
}
@ -82,7 +82,7 @@ onMounted(() => {
enableAutoReload: true,
load(el) {
previewImage.value.addEventListener(
"load",
'load',
onThumbnailPreviewLoaded,
);
previewImage.value.src = props.blurredPreviewSrc;
@ -91,43 +91,39 @@ onMounted(() => {
observer.observe();
});
// callback for fully-loaded image event
const onThumbnailFullyLoaded = () => {
fullImage.value.removeEventListener("load", onThumbnailFullyLoaded);
fullImage.value.removeEventListener('load', onThumbnailFullyLoaded);
pictureElement.value.removeChild(previewImage.value);
isLoading.value = false;
}
};
// callback for preview-image-loaded event
const onThumbnailPreviewLoaded = () => {
// cleanup event listener on preview
previewImage.value.removeEventListener(
"load",
onThumbnailPreviewLoaded,
);
previewImage.value.removeEventListener('load', onThumbnailPreviewLoaded);
// add event listener for full-resolution image
fullImage.value.addEventListener("load", onThumbnailFullyLoaded);
fullImage.value.addEventListener('load', onThumbnailFullyLoaded);
fullImage.value.src = props.lazySrc;
isPreviewLoading.value = false;
};
onUnmounted(() => {
if(previewImage.value !== 'undefined' && previewImage.value != null){
if (previewImage.value !== 'undefined' && previewImage.value != null) {
previewImage.value.removeEventListener(
"load",
'load',
onThumbnailPreviewLoaded,
);
}
if(fullImage.value !== 'undefined' && fullImage.value != null) {
fullImage.value.removeEventListener("load", onThumbnailFullyLoaded);
if (fullImage.value !== 'undefined' && fullImage.value != null) {
fullImage.value.removeEventListener('load', onThumbnailFullyLoaded);
}
});
</script>
<script>
export default {
name: 'LazyPicture'
name: 'LazyPicture',
};
</script>

Просмотреть файл

@ -1,4 +1,4 @@
import moment from "@nextcloud/moment";
import moment from '@nextcloud/moment';
/**
* The schema.org standard requires the dates formatted as Date (https://schema.org/Date)
@ -6,11 +6,11 @@ import moment from "@nextcloud/moment";
* @param dt
* @returns {null|moment.Moment|*}
*/
export function parseDateTime(dt) {
export function parseDateTime(dt) {
if (!dt) return null;
const date = moment(dt, moment.ISO_8601);
if (!date.isValid()) {
return null;
}
return date;
};
}

Просмотреть файл

@ -22,20 +22,21 @@
*
*/
import { readonly, ref } from 'vue'
import { readonly, ref } from 'vue';
/**
* The minimal width of the viewport to be considered a desktop device
*/
export const MOBILE_BREAKPOINT = 1024
export const MOBILE_BREAKPOINT = 1024;
const checkIfIsMobile = () => document.documentElement.clientWidth < MOBILE_BREAKPOINT
const checkIfIsMobile = () =>
document.documentElement.clientWidth < MOBILE_BREAKPOINT;
const isMobile = ref(checkIfIsMobile())
const isMobile = ref(checkIfIsMobile());
window.addEventListener('resize', () => {
isMobile.value = checkIfIsMobile()
})
isMobile.value = checkIfIsMobile();
});
/**
* Use global isMobile state, based on the viewport width
@ -43,7 +44,7 @@ window.addEventListener('resize', () => {
* @return {import('vue').DeepReadonly<import('vue').Ref<boolean>>}
*/
export function useIsMobile() {
return readonly(isMobile)
return readonly(isMobile);
}
/**
@ -51,4 +52,4 @@ export function useIsMobile() {
* Use `composables/useIsMobile` instead.
* Defined and exported only for isMobile mixin.
*/
export const isMobileState = readonly(isMobile)
export const isMobileState = readonly(isMobile);

Просмотреть файл

@ -1,14 +1,22 @@
import {position as caretPosition} from "caret-pos";
import { computed, nextTick, ref } from "vue";
import { position as caretPosition } from 'caret-pos';
import { computed, nextTick, ref } from 'vue';
export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursorPosition, suggestionsData, buffer, emit, log, props) {
export default function useSuggestionsPopup(
suggestionsPopupElementA,
lastCursorPosition,
suggestionsData,
buffer,
emit,
log,
props,
) {
const clamp = (val, min, max) => Math.min(max, Math.max(min, val));
/**
* Reference to the SuggestionsPopup DOM element.
* @type {Ref<HTMLElement | null>}
*/
const suggestionsPopupElement = ref(null)
const suggestionsPopupElement = ref(null);
/**
* Cancel the suggestions popup by setting the data object to null
@ -18,9 +26,7 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
};
const suggestionsPopupVisible = computed(() => {
return (
suggestionsData.value !== null && !suggestionsData.value.blurred
);
return suggestionsData.value !== null && !suggestionsData.value.blurred;
});
const filteredSuggestionOptions = computed(() => {
@ -28,10 +34,8 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
return props.suggestionOptions.filter(
(option) =>
searchText === "" ||
option.title
.toLowerCase()
.includes(searchText.toLowerCase()),
searchText === '' ||
option.title.toLowerCase().includes(searchText.toLowerCase()),
);
});
@ -41,7 +45,7 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
*/
const handleSuggestionsPopupSelectedEvent = (opt) => {
handleSuggestionsPopupSelected(opt.recipe_id);
}
};
/**
* Handle something selected by click or by `Enter`
@ -59,9 +63,9 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
if (buffer?.value) {
buffer.value[suggestionsData.value.fieldIndex] = newValue;
emit("input", buffer.value);
emit('input', buffer.value);
} else {
emit("input", newValue);
emit('input', newValue);
}
handleSuggestionsPopupCancel();
// set cursor to position after pasted string. Waiting two ticks is necessary for
@ -82,26 +86,26 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
const handleSuggestionsPopupOpenKeyUp = (e, cursorPos) => {
const caretPos = caretPosition(e.target, {
customPos: suggestionsData.value.hashPosition - 1,
})
});
// Only update the popover position if the line changes (caret pos y changes)
if (caretPos.top !== suggestionsData.value.caretPos.top) {
suggestionsData.value.caretPos = caretPos
suggestionsData.value.caretPos = caretPos;
}
// Cancel suggestion popup on whitespace or caret movement
if (
[
" ",
"\t",
"#",
"ArrowLeft",
"ArrowRight",
"Home",
"End",
"PageUp",
"PageDown",
"Escape",
' ',
'\t',
'#',
'ArrowLeft',
'ArrowRight',
'Home',
'End',
'PageUp',
'PageDown',
'Escape',
].includes(e.key)
) {
handleSuggestionsPopupCancel();
@ -126,8 +130,8 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
};
const getClosestListItemIndex = (field) => {
const $li = field.closest("li");
const $ul = $li?.closest("ul");
const $li = field.closest('li');
const $ul = $li?.closest('ul');
if (!$ul) return null;
return Array.prototype.indexOf.call($ul.childNodes, $li);
@ -149,24 +153,21 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
}
// Only do anything for # key
if (e.key !== "#") return;
if (e.key !== '#') return;
// Show the popup only if the # was inserted at the very
// beginning of the input or after any whitespace character
if (
!(
cursorPos === 1 ||
/\s/.test(field.value.charAt(cursorPos - 2))
)
!(cursorPos === 1 || /\s/.test(field.value.charAt(cursorPos - 2)))
) {
return;
}
// Show dialog to select recipe
const caretPos = caretPosition(field, { customPos: cursorPos - 1 })
const caretPos = caretPosition(field, { customPos: cursorPos - 1 });
suggestionsData.value = {
field,
searchText: "",
searchText: '',
caretPos,
focusIndex: 0,
hashPosition: cursorPos,
@ -190,17 +191,17 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
*/
const handleSuggestionsPopupOpenKeyDown = (e) => {
// Handle switching the focused option with up/down keys
if (["ArrowUp", "ArrowDown"].includes(e.key)) {
if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
e.preventDefault();
// Increment/decrement focuse index based on which key was pressed
// and constrain between 0 and length - 1
const focusIndex = clamp(
suggestionsData.value.focusIndex +
{
ArrowUp: -1,
ArrowDown: +1,
}[e.key],
{
ArrowUp: -1,
ArrowDown: +1,
}[e.key],
0,
props.suggestionOptions.length - 1,
);
@ -212,7 +213,7 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
}
// Handle selecting the current option when enter is pressed
if (e.key === "Enter") {
if (e.key === 'Enter') {
e.preventDefault();
const { focusIndex } = suggestionsData.value;
const selection = filteredSuggestionOptions.value[focusIndex];
@ -224,9 +225,9 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
* Recover suggestions popup on focus
*/
const handleSuggestionsPopupFocus = (e) => {
log.debug("focus", e, JSON.stringify(suggestionsData.value))
log.debug('focus', e, JSON.stringify(suggestionsData.value));
if (suggestionsData.value?.blurred) {
suggestionsData.value.blurred = false
suggestionsData.value.blurred = false;
}
};
@ -234,7 +235,7 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
* Cancel selection if input gets blurred
*/
const handleSuggestionsPopupBlur = (e) => {
log.debug("blur", e, JSON.stringify(suggestionsData.value))
log.debug('blur', e, JSON.stringify(suggestionsData.value));
if (!suggestionsPopupVisible.value || !suggestionsPopupElement.value) {
return;
}
@ -281,6 +282,6 @@ export default function useSuggestionsPopup(suggestionsPopupElementA, lastCursor
handleSuggestionsPopupFocus,
handleSuggestionsPopupBlur,
handleSuggestionsPopupMouseUp,
handleSuggestionsPopupSelectedEvent
handleSuggestionsPopupSelectedEvent,
};
}

Просмотреть файл

@ -5,33 +5,33 @@
* @license AGPL3 or later
*/
import Vue from "vue"
import Vue from 'vue';
import { useStore } from "./store"
import { useStore } from './store';
import AppInvalidGuest from "./components/AppInvalidGuest.vue"
import AppInvalidGuest from './components/AppInvalidGuest.vue';
// eslint-disable-next-line camelcase,no-undef
if (__webpack_use_dev_server__ || false) {
// eslint-disable-next-line camelcase,no-undef
__webpack_public_path__ = "http://127.0.0.1:3000/apps/cookbook/js/"
__webpack_public_path__ = 'http://127.0.0.1:3000/apps/cookbook/js/';
}
// Fetch Nextcloud nonce identifier for dynamic script loading
// eslint-disable-next-line camelcase,no-undef
__webpack_nonce__ = btoa(OC.requestToken)
__webpack_nonce__ = btoa(OC.requestToken);
// Also make the injections available in Vue components
Vue.prototype.OC = OC
Vue.prototype.OC = OC;
// Pass translation engine to Vue
Vue.prototype.t = window.t
Vue.prototype.t = window.t;
const store = useStore();
// Start the app once document is done loading
const App = Vue.extend(AppInvalidGuest)
const App = Vue.extend(AppInvalidGuest);
new App({
store,
// router,
}).$mount("#content")
}).$mount('#content');

Просмотреть файл

@ -1,115 +1,115 @@
import Vue from "vue"
import axios from "@nextcloud/axios"
import Vue from 'vue';
import axios from '@nextcloud/axios';
import { generateUrl } from "@nextcloud/router"
import { generateUrl } from '@nextcloud/router';
const instance = axios.create()
const instance = axios.create();
const baseUrl = `${generateUrl("apps/cookbook")}/webapp`
const baseUrl = `${generateUrl('apps/cookbook')}/webapp`;
// Add a debug log for every request
instance.interceptors.request.use((config) => {
Vue.$log.debug(
`[axios] Making "${config.method}" request to "${config.url}"`,
config,
)
const contentType = config.headers["Content-Type"]
);
const contentType = config.headers['Content-Type'];
if (
contentType &&
!["application/json", "text/json"].includes(contentType)
!['application/json', 'text/json'].includes(contentType)
) {
Vue.$log.warn(
`[axios] Request to "${config.url}" is using Content-Type "${contentType}", not JSON`,
)
);
}
return config
})
return config;
});
instance.interceptors.response.use(
(response) => {
Vue.$log.debug("[axios] Received response", response)
return response
Vue.$log.debug('[axios] Received response', response);
return response;
},
(error) => {
Vue.$log.warn("[axios] Received error", error)
return Promise.reject(error)
Vue.$log.warn('[axios] Received error', error);
return Promise.reject(error);
},
)
);
axios.defaults.headers.common.Accept = "application/json"
axios.defaults.headers.common.Accept = 'application/json';
function createNewRecipe(recipe) {
return instance.post(`${baseUrl}/recipes`, recipe)
return instance.post(`${baseUrl}/recipes`, recipe);
}
function getRecipe(id) {
return instance.get(`${baseUrl}/recipes/${id}`)
return instance.get(`${baseUrl}/recipes/${id}`);
}
function getAllRecipes() {
return instance.get(`${baseUrl}/recipes`)
return instance.get(`${baseUrl}/recipes`);
}
function getAllRecipesOfCategory(categoryName) {
return instance.get(`${baseUrl}/category/${categoryName}`)
return instance.get(`${baseUrl}/category/${categoryName}`);
}
function getAllRecipesWithTag(tags) {
return instance.get(`${baseUrl}/tags/${tags}`)
return instance.get(`${baseUrl}/tags/${tags}`);
}
function searchRecipes(search) {
return instance.get(`${baseUrl}/search/${search}`)
return instance.get(`${baseUrl}/search/${search}`);
}
function updateRecipe(id, recipe) {
return instance.put(`${baseUrl}/recipes/${id}`, recipe)
return instance.put(`${baseUrl}/recipes/${id}`, recipe);
}
function deleteRecipe(id) {
return instance.delete(`${baseUrl}/recipes/${id}`)
return instance.delete(`${baseUrl}/recipes/${id}`);
}
function importRecipe(url) {
return instance.post(`${baseUrl}/import`, `url=${url}`)
return instance.post(`${baseUrl}/import`, `url=${url}`);
}
function getAllCategories() {
return instance.get(`${baseUrl}/categories`)
return instance.get(`${baseUrl}/categories`);
}
function updateCategoryName(oldName, newName) {
return instance.put(`${baseUrl}/category/${encodeURIComponent(oldName)}`, {
name: newName,
})
});
}
function getAllKeywords() {
return instance.get(`${baseUrl}/keywords`)
return instance.get(`${baseUrl}/keywords`);
}
function getConfig() {
return instance.get(`${baseUrl}/config`)
return instance.get(`${baseUrl}/config`);
}
function updatePrintImageSetting(enabled) {
return instance.post(`${baseUrl}/config`, { print_image: enabled ? 1 : 0 })
return instance.post(`${baseUrl}/config`, { print_image: enabled ? 1 : 0 });
}
function updateUpdateInterval(newInterval) {
return instance.post(`${baseUrl}/config`, { update_interval: newInterval })
return instance.post(`${baseUrl}/config`, { update_interval: newInterval });
}
function updateRecipeDirectory(newDir) {
return instance.post(`${baseUrl}/config`, { folder: newDir })
return instance.post(`${baseUrl}/config`, { folder: newDir });
}
function updateVisibleInfoBlocks(visibleInfoBlocks) {
return instance.post(`${baseUrl}/config`, { visibleInfoBlocks })
return instance.post(`${baseUrl}/config`, { visibleInfoBlocks });
}
function reindex() {
return instance.post(`${baseUrl}/reindex`)
return instance.post(`${baseUrl}/reindex`);
}
export default {
@ -147,4 +147,4 @@ export default {
update: updateVisibleInfoBlocks,
},
},
}
};

Просмотреть файл

@ -1,16 +1,16 @@
import { showSimpleAlertModal } from "cookbook/js/modals"
import { showSimpleAlertModal } from 'cookbook/js/modals';
// Check if two routes point to the same component but have different content
function shouldReloadContent(url1, url2) {
if (url1 === url2) {
return false // Obviously should not if both routes are the same
return false; // Obviously should not if both routes are the same
}
const comps1 = url1.split("/")
const comps2 = url2.split("/")
const comps1 = url1.split('/');
const comps2 = url2.split('/');
if (comps1.length < 2 || comps2.length < 2) {
return false // Just a failsafe, this should never happen
return false; // Just a failsafe, this should never happen
}
// The route structure is as follows:
@ -20,47 +20,47 @@ function shouldReloadContent(url1, url2) {
// If the items are different, then the router automatically handles
// component loading: do not manually reload
if (comps1[1] !== comps2[1]) {
return false
return false;
}
// If one of the routes is edit and the other is not
if (comps1.length !== comps2.length) {
// Only reload if changing from edit to create
return comps1.pop() === "create" || comps2.pop() === "create"
return comps1.pop() === 'create' || comps2.pop() === 'create';
}
if (comps1.pop() === "create") {
if (comps1.pop() === 'create') {
// But, if we are moving from create to view, do not reload
// the create component
return false
return false;
}
// Only options left are that both of the routes are edit or view,
// but not identical, or that we're moving from view to create
// -> reload view
return true
return true;
}
// Check if the two urls point to the same item instance
function isSameItemInstance(url1, url2) {
if (url1 === url2) {
return true // Obviously true if the routes are the same
return true; // Obviously true if the routes are the same
}
const comps1 = url1.split("/")
const comps2 = url2.split("/")
const comps1 = url1.split('/');
const comps2 = url2.split('/');
if (comps1.length < 2 || comps2.length < 2) {
return false // Just a failsafe, this should never happen
return false; // Just a failsafe, this should never happen
}
// If the items are different, then the item instance cannot be
// the same either
if (comps1[1] !== comps2[1]) {
return false
return false;
}
if (comps1.length < 3 || comps2.length < 3) {
// ID is the third url component, so can't be the same instance if
// either of the urls have less than three components
return false
return false;
}
return comps1[2] === comps2[2]
return comps1[2] === comps2[2];
}
// A simple function to sanitize HTML tags
@ -69,13 +69,13 @@ function escapeHTML(text) {
/["&'<>]/g,
(a) =>
({
"&": "&amp;",
'"': "&quot;",
"'": "&apos;",
"<": "&lt;",
">": "&gt;",
'&': '&amp;',
'"': '&quot;',
"'": '&apos;',
'<': '&lt;',
'>': '&gt;',
})[a],
)
);
}
// Fix the decimal separator for languages that use a comma instead of dot
@ -84,68 +84,68 @@ function fixDecimalSeparator(value, io) {
// value is the string value of the number to process
// io is either 'i' as in input or 'o' as in output
if (!value) {
return ""
return '';
}
if (io === "i") {
if (io === 'i') {
// Check if it's an American number where a comma precedes a dot
// e.g. 12,500.25
if (value.indexOf(".") > value.indexOf(",")) {
return value.replace(",", "")
if (value.indexOf('.') > value.indexOf(',')) {
return value.replace(',', '');
}
return value.replace(",", ".")
return value.replace(',', '.');
}
if (io === "o") {
return value.toString().replace(".", ",")
if (io === 'o') {
return value.toString().replace('.', ',');
}
return ""
return '';
}
// This will replace the PHP function nl2br in Vue components
// deprecated
function nl2br(text) {
return text.replace(/\n/g, "<br />")
return text.replace(/\n/g, '<br />');
}
// A simple function that converts a MySQL datetime into a timestamp.
// deprecated
function getTimestamp(date) {
if (date) {
return new Date(date)
return new Date(date);
}
return null
return null;
}
let router
let router;
function useRouter(_router) {
router = _router
router = _router;
}
// Push a new URL to the router, essentially navigating to that page.
function goTo(url) {
router.push(url)
router.push(url);
}
// Notify the user if notifications are allowed
// deprecated
function notify(title, options) {
if (!("Notification" in window)) {
return
if (!('Notification' in window)) {
return;
}
if (Notification.permission === "granted") {
if (Notification.permission === 'granted') {
// eslint-disable-next-line no-unused-vars
const notification = new Notification(title, options)
} else if (Notification.permission !== "denied") {
const notification = new Notification(title, options);
} else if (Notification.permission !== 'denied') {
Notification.requestPermission((permission) => {
if (!("permission" in Notification)) {
Notification.permission = permission
if (!('permission' in Notification)) {
Notification.permission = permission;
}
if (permission === "granted") {
if (permission === 'granted') {
// eslint-disable-next-line no-unused-vars
const notification = new Notification(title, options)
const notification = new Notification(title, options);
} else {
showSimpleAlertModal(title)
showSimpleAlertModal(title);
}
})
});
}
}
@ -159,4 +159,4 @@ export default {
useRouter,
goTo,
notify,
}
};

Просмотреть файл

@ -1,14 +1,14 @@
// TODO: Switch to vuejs3-logger when we switch to Vue 3
import VueLogger from "vuejs-logger"
import moment from "@nextcloud/moment"
import VueLogger from 'vuejs-logger';
import moment from '@nextcloud/moment';
const DEFAULT_LOG_LEVEL = "info"
const DEFAULT_LOG_LEVEL = 'info';
// How many minutes the logging configuration is valid for
const EXPIRY_MINUTES = 30
const EXPIRY_MINUTES = 30;
// localStorage keys
const KEY_ENABLED = "COOKBOOK_LOGGING_ENABLED"
const KEY_EXPIRY = "COOKBOOK_LOGGING_EXPIRY"
const KEY_LOG_LEVEL = "COOKBOOK_LOGGING_LEVEL"
const KEY_ENABLED = 'COOKBOOK_LOGGING_ENABLED';
const KEY_EXPIRY = 'COOKBOOK_LOGGING_EXPIRY';
const KEY_LOG_LEVEL = 'COOKBOOK_LOGGING_LEVEL';
// Check if the logging configuration in local storage has expired
//
@ -19,40 +19,40 @@ const KEY_LOG_LEVEL = "COOKBOOK_LOGGING_LEVEL"
// logging. We don't want them to have to setup the expiry as well
const isExpired = (timestamp) => {
if (timestamp === null) {
return false
return false;
}
return moment().isAfter(parseInt(timestamp, 10))
}
return moment().isAfter(parseInt(timestamp, 10));
};
const isEnabled = () => {
const DEFAULT = false
const userValue = localStorage.getItem(KEY_ENABLED)
const expiry = localStorage.getItem(KEY_EXPIRY)
const DEFAULT = false;
const userValue = localStorage.getItem(KEY_ENABLED);
const expiry = localStorage.getItem(KEY_EXPIRY);
// Detect the first load after the user has enabled logging
// Set the expiry so the logging isn't enabled forever
if (userValue !== null && expiry === null) {
localStorage.setItem(
KEY_EXPIRY,
moment().add(EXPIRY_MINUTES, "m").valueOf(),
)
moment().add(EXPIRY_MINUTES, 'm').valueOf(),
);
}
if (isExpired(expiry)) {
localStorage.removeItem(KEY_ENABLED)
localStorage.removeItem(KEY_EXPIRY)
localStorage.removeItem(KEY_ENABLED);
localStorage.removeItem(KEY_EXPIRY);
return DEFAULT
return DEFAULT;
}
// Local storage converts everything to string
// Use JSON.parse to transform "false" -> false
return JSON.parse(userValue) ?? DEFAULT
}
return JSON.parse(userValue) ?? DEFAULT;
};
export default function setupLogging(Vue) {
const logLevel = localStorage.getItem(KEY_LOG_LEVEL) ?? DEFAULT_LOG_LEVEL
const logLevel = localStorage.getItem(KEY_LOG_LEVEL) ?? DEFAULT_LOG_LEVEL;
Vue.use(VueLogger, {
isEnabled: isEnabled(),
@ -60,13 +60,13 @@ export default function setupLogging(Vue) {
stringifyArguments: false,
showLogLevel: true,
showMethodName: true,
separator: "|",
separator: '|',
showConsoleColors: true,
})
});
Vue.$log.info(`Setting up logging with log level ${logLevel}`)
Vue.$log.info(`Setting up logging with log level ${logLevel}`);
}
export function enableLogging() {
localStorage.setItem(KEY_ENABLED, true)
localStorage.setItem(KEY_ENABLED, true);
}

Просмотреть файл

@ -1,11 +1,15 @@
import { create } from "vue-modal-dialogs"
import { create } from 'vue-modal-dialogs';
import SimpleAlertModal from "../components/Modals/SimpleAlertModal.vue"
import SimpleConfirmModal from "../components/Modals/SimpleConfirmModal.vue"
import SimpleAlertModal from '../components/Modals/SimpleAlertModal.vue';
import SimpleConfirmModal from '../components/Modals/SimpleConfirmModal.vue';
export const showSimpleAlertModal = create(SimpleAlertModal, "content", "title")
export const showSimpleAlertModal = create(
SimpleAlertModal,
'content',
'title',
);
export const showSimpleConfirmModal = create(
SimpleConfirmModal,
"content",
"title",
)
'content',
'title',
);

Просмотреть файл

@ -1,85 +1,85 @@
import api from "cookbook/js/api-interface"
import api from 'cookbook/js/api-interface';
import { generateUrl } from "@nextcloud/router"
import { generateUrl } from '@nextcloud/router';
const baseUrl = generateUrl("apps/cookbook")
const baseUrl = generateUrl('apps/cookbook');
function extractAllRecipeLinkIds(content) {
const re = /(?:^|[^#])#r\/([0-9]+)/g
let ret = []
let matches
const re = /(?:^|[^#])#r\/([0-9]+)/g;
let ret = [];
let matches;
for (
matches = re.exec(content);
matches !== null;
matches = re.exec(content)
) {
ret.push(matches[1])
ret.push(matches[1]);
}
// Make the ids unique, see https://stackoverflow.com/a/14438954/882756
function onlyUnique(value, index, self) {
return self.indexOf(value) === index
return self.indexOf(value) === index;
}
ret = ret.filter(onlyUnique)
ret = ret.filter(onlyUnique);
return ret
return ret;
}
async function getRecipesFromLinks(linkIds) {
return Promise.all(
linkIds.map(async (x) => {
let recipe
let recipe;
try {
recipe = await api.recipes.get(x)
recipe = await api.recipes.get(x);
} catch (ex) {
recipe = null
recipe = null;
}
return recipe
return recipe;
}),
)
);
}
function cleanUpRecipeList(recipes) {
return recipes.filter((r) => r !== null).map((x) => x.data)
return recipes.filter((r) => r !== null).map((x) => x.data);
}
function getRecipeUrl(id) {
return `${baseUrl}/#/recipe/${id}`
return `${baseUrl}/#/recipe/${id}`;
}
function insertMarkdownLinks(content, recipes) {
let ret = content
let ret = content;
recipes.forEach((r) => {
const { id } = r
const { id } = r;
// Replace link urls in dedicated links (like [this example](#r/123))
ret = ret.replace(`](${id})`, `](${getRecipeUrl(id)})`)
ret = ret.replace(`](${id})`, `](${getRecipeUrl(id)})`);
// Replace plain references with recipe name
const rePlain = RegExp(
`(^|\\s|[,._+&?!-])#r/${id}($|\\s|[,._+&?!-])`,
"g",
)
'g',
);
// const re = /(^|\s|[,._+&?!-])#r\/(\d+)(?=$|\s|[.,_+&?!-])/g
ret = ret.replace(
rePlain,
`$1[${r.name} (\\#r/${id})](${getRecipeUrl(id)})$2`,
)
})
return ret
);
});
return ret;
}
async function normalizeNamesMarkdown(content) {
// console.log(`Content: ${content}`)
const linkIds = extractAllRecipeLinkIds(content)
let recipes = await getRecipesFromLinks(linkIds)
recipes = cleanUpRecipeList(recipes)
const linkIds = extractAllRecipeLinkIds(content);
let recipes = await getRecipesFromLinks(linkIds);
recipes = cleanUpRecipeList(recipes);
// console.log("List of recipes", recipes)
const markdown = insertMarkdownLinks(content, recipes)
const markdown = insertMarkdownLinks(content, recipes);
// console.log("Formatted markdown:", markdown)
return markdown
return markdown;
}
export default normalizeNamesMarkdown
export default normalizeNamesMarkdown;

Просмотреть файл

@ -2,7 +2,7 @@
The ingredientFractionRegExp is used to identify fractions in the string.
This is used to exclude strings that contain fractions from being valid.
*/
const fractionRegExp = /^((\d+\s+)?(\d+)\s*\/\s*(\d+)).*/
const fractionRegExp = /^((\d+\s+)?(\d+)\s*\/\s*(\d+)).*/;
function isValidIngredientSyntax(ingredient) {
/*
@ -10,29 +10,29 @@ function isValidIngredientSyntax(ingredient) {
possibly followed by a fractional part or a fraction. Then there should be a space
and then any sequence of characters.
*/
const ingredientSyntaxRegExp = /^(?:\d+(?:\.\d+)?(?:\/\d+)?)\s?.*$/
const ingredientSyntaxRegExp = /^(?:\d+(?:\.\d+)?(?:\/\d+)?)\s?.*$/;
/*
The ingredientMultipleSeperatorsRegExp is used to check whether the string contains
more than one separators (.,) after a number. This is used to exclude strings that
contain more than one separator from being valid.
*/
const ingredientMultipleSeperatorsRegExp = /^-?\d+(?:[.,]\d+){2,}.*/
const ingredientMultipleSeperatorsRegExp = /^-?\d+(?:[.,]\d+){2,}.*/;
return (
fractionRegExp.test(ingredient) ||
(ingredientSyntaxRegExp.test(ingredient) &&
!ingredientMultipleSeperatorsRegExp.test(ingredient))
)
);
}
function isIngredientsArrayValid(ingredients) {
return ingredients.every(isValidIngredientSyntax)
return ingredients.every(isValidIngredientSyntax);
}
function recalculateIngredients(ingredients, currentYield, originalYield) {
return ingredients.map((ingredient) => {
const matches = ingredient.match(fractionRegExp)
const matches = ingredient.match(fractionRegExp);
if (matches) {
const [
@ -41,42 +41,42 @@ function recalculateIngredients(ingredients, currentYield, originalYield) {
wholeNumberPartRaw,
numeratorRaw,
denominatorRaw,
] = matches
] = matches;
const wholeNumberPart = wholeNumberPartRaw
? parseInt(wholeNumberPartRaw, 10)
: 0
const numerator = parseInt(numeratorRaw, 10)
const denominator = parseInt(denominatorRaw, 10)
: 0;
const numerator = parseInt(numeratorRaw, 10);
const denominator = parseInt(denominatorRaw, 10);
const decimalAmount = wholeNumberPart + numerator / denominator
let newAmount = (decimalAmount / originalYield) * currentYield
newAmount = newAmount.toFixed(2).replace(/[.]00$/, "")
const decimalAmount = wholeNumberPart + numerator / denominator;
let newAmount = (decimalAmount / originalYield) * currentYield;
newAmount = newAmount.toFixed(2).replace(/[.]00$/, '');
const newIngredient = ingredient.replace(fractionMatch, newAmount)
return newIngredient
const newIngredient = ingredient.replace(fractionMatch, newAmount);
return newIngredient;
}
if (isValidIngredientSyntax(ingredient)) {
const possibleUnit = ingredient
.split(" ")[0]
.replace(/[^a-zA-Z]/g, "")
.split(' ')[0]
.replace(/[^a-zA-Z]/g, '');
const amount = parseFloat(
ingredient.split(" ")[0].replace(",", "."),
)
const unitAndIngredient = ingredient.split(" ").slice(1).join(" ")
ingredient.split(' ')[0].replace(',', '.'),
);
const unitAndIngredient = ingredient.split(' ').slice(1).join(' ');
let newAmount = (amount / originalYield) * currentYield
newAmount = newAmount.toFixed(2).replace(/[.]00$/, "")
let newAmount = (amount / originalYield) * currentYield;
newAmount = newAmount.toFixed(2).replace(/[.]00$/, '');
return `${newAmount}${possibleUnit} ${unitAndIngredient}`
return `${newAmount}${possibleUnit} ${unitAndIngredient}`;
}
return ingredient
})
return ingredient;
});
}
export default {
isValidIngredientSyntax,
isIngredientsArrayValid,
recalculateIngredients,
}
};

Просмотреть файл

@ -6,69 +6,69 @@
*/
// Markdown
import VueShowdown from "vue-showdown"
import VueShowdown from 'vue-showdown';
import Vue from "vue"
import Vue from 'vue';
import * as ModalDialogs from "vue-modal-dialogs"
import * as ModalDialogs from 'vue-modal-dialogs';
import helpers from "cookbook/js/helper"
import setupLogging from "cookbook/js/logging"
import helpers from 'cookbook/js/helper';
import setupLogging from 'cookbook/js/logging';
import { linkTo } from "@nextcloud/router"
import { linkTo } from '@nextcloud/router';
import router from "./router"
import { useStore } from "./store"
import router from './router';
import { useStore } from './store';
import AppMain from "./components/AppMain.vue"
import AppMain from './components/AppMain.vue';
// eslint-disable-next-line camelcase,no-undef
if (__webpack_use_dev_server__ || false) {
// eslint-disable-next-line camelcase,no-undef
__webpack_public_path__ = "http://127.0.0.1:3000/apps/cookbook/js/"
__webpack_public_path__ = 'http://127.0.0.1:3000/apps/cookbook/js/';
}
// eslint-disable-next-line camelcase,no-undef
__webpack_public_path__ = `${linkTo("cookbook", "js")}/`
__webpack_public_path__ = `${linkTo('cookbook', 'js')}/`;
// Fetch Nextcloud nonce identifier for dynamic script loading
// eslint-disable-next-line camelcase,no-undef
__webpack_nonce__ = btoa(OC.requestToken)
__webpack_nonce__ = btoa(OC.requestToken);
helpers.useRouter(router)
helpers.useRouter(router);
// A simple function to sanitize HTML tags
// eslint-disable-next-line no-param-reassign
window.escapeHTML = helpers.escapeHTML
window.escapeHTML = helpers.escapeHTML;
// Also make the injections available in Vue components
Vue.prototype.$window = window
Vue.prototype.OC = OC
Vue.prototype.$window = window;
Vue.prototype.OC = OC;
// Markdown for Vue
Vue.use(VueShowdown, {
// set default flavor for Markdown
flavor: "vanilla",
})
flavor: 'vanilla',
});
// TODO: Equivalent library for Vue3 when we make that transition:
// https://github.com/rlemaigre/vue3-promise-dialog
Vue.use(ModalDialogs)
Vue.use(ModalDialogs);
setupLogging(Vue)
setupLogging(Vue);
// Pass translation engine to Vue
Vue.prototype.t = window.t
Vue.prototype.t = window.t;
const store = useStore();
// Start the app once document is done loading
Vue.$log.info("Main is done. Creating App.")
const App = Vue.extend(AppMain)
Vue.$log.info('Main is done. Creating App.');
const App = Vue.extend(AppMain);
new App({
store,
router,
beforeCreate() {
this.$store.commit("initializeStore")
this.$store.commit('initializeStore');
},
}).$mount("#content")
}).$mount('#content');

Просмотреть файл

@ -4,16 +4,16 @@
* ----------------------
* @license AGPL3 or later
*/
import Vue from "vue"
import VueRouter from "vue-router"
import Vue from 'vue';
import VueRouter from 'vue-router';
import Index from "../components/AppIndex.vue"
import NotFound from "../components/NotFound.vue"
import RecipeView from "../components/RecipeView/RecipeView.vue"
import RecipeEdit from "../components/RecipeEdit.vue"
import Search from "../components/SearchResults.vue"
import Index from '../components/AppIndex.vue';
import NotFound from '../components/NotFound.vue';
import RecipeView from '../components/RecipeView/RecipeView.vue';
import RecipeEdit from '../components/RecipeEdit.vue';
import Search from '../components/SearchResults.vue';
Vue.use(VueRouter)
Vue.use(VueRouter);
// The router will try to match routers in a descending order.
// Routes that share the same root, must be listed from the
@ -26,28 +26,28 @@ Vue.use(VueRouter)
const routes = [
// Search routes
{
path: "/category/:value",
name: "search-category",
path: '/category/:value',
name: 'search-category',
component: Search,
props: { query: "cat" },
props: { query: 'cat' },
},
{
path: "/name/:value",
name: "search-name",
path: '/name/:value',
name: 'search-name',
component: Search,
props: { query: "name" },
props: { query: 'name' },
},
{
path: "/search/:value",
name: "search-general",
path: '/search/:value',
name: 'search-general',
component: Search,
props: { query: "general" },
props: { query: 'general' },
},
{
path: "/tags/:value",
name: "search-tags",
path: '/tags/:value',
name: 'search-tags',
component: Search,
props: { query: "tags" },
props: { query: 'tags' },
},
// Recipe routes
@ -59,17 +59,17 @@ const routes = [
// - View: /{item}/:id
// - Edit: /{item}/:id/edit
// - Create: /{item}/create
{ path: "/recipe/create", name: "recipe-create", component: RecipeEdit },
{ path: "/recipe/:id/edit", name: "recipe-edit", component: RecipeEdit },
{ path: "/recipe/:id", name: "recipe-view", component: RecipeView },
{ path: '/recipe/create', name: 'recipe-create', component: RecipeEdit },
{ path: '/recipe/:id/edit', name: 'recipe-edit', component: RecipeEdit },
{ path: '/recipe/:id', name: 'recipe-view', component: RecipeView },
// Index is the last defined route
{ path: "/", name: "index", component: Index },
{ path: '/', name: 'index', component: Index },
// Anything not matched goes to NotFound
{ path: "*", name: "not-found", component: NotFound },
]
{ path: '*', name: 'not-found', component: NotFound },
];
export default new VueRouter({
routes,
})
});

Просмотреть файл

@ -1,4 +1,4 @@
extends: "../../.eslintrc.yml"
extends: '../../.eslintrc.yml'
rules:
no-param-reassign:

Просмотреть файл

@ -4,11 +4,11 @@
* ----------------------
* @license AGPL3 or later
*/
import Vue from "vue"
import Vuex from "vuex"
import api from "cookbook/js/api-interface"
import Vue from 'vue';
import Vuex from 'vuex';
import api from 'cookbook/js/api-interface';
Vue.use(Vuex)
Vue.use(Vuex);
// We are using the vuex store linking changes within the components to updates in the navigation panel.
var store = new Vuex.Store({
@ -31,7 +31,7 @@ var store = new Vuex.Store({
// several independent components
recipe: null,
// Filter applied to a list of recipes
recipeFilters: "",
recipeFilters: '',
// Loading and saving states to determine which loader icons to show.
// State of -1 is reserved for recipe and edit views to be set when the
// User loads the app at one of these locations and has to wait for an
@ -53,68 +53,68 @@ var store = new Vuex.Store({
mutations: {
setConfig(state, { config }) {
state.config = config
state.config = config;
},
initializeStore(state) {
if (localStorage.getItem("showTagCloudInRecipeList")) {
if (localStorage.getItem('showTagCloudInRecipeList')) {
state.localSettings.showTagCloudInRecipeList = JSON.parse(
localStorage.getItem("showTagCloudInRecipeList"),
)
localStorage.getItem('showTagCloudInRecipeList'),
);
} else {
state.localSettings.showTagCloudInRecipeList = true
state.localSettings.showTagCloudInRecipeList = true;
}
},
setAppNavigationRefreshRequired(state, { b }) {
state.appNavigation.refreshRequired = b
state.appNavigation.refreshRequired = b;
},
setAppNavigationVisible(state, { b }) {
state.appNavigation.visible = b
state.appNavigation.visible = b;
},
setCategoryUpdating(state, { c }) {
state.categoryUpdating = c
state.categoryUpdating = c;
},
setLoadingRecipe(state, { r }) {
state.loadingRecipe = r
state.loadingRecipe = r;
},
setPage(state, { p }) {
state.page = p
state.page = p;
},
setRecipe(state, { r }) {
const rec = JSON.parse(JSON.stringify(r))
const rec = JSON.parse(JSON.stringify(r));
if (rec === null) {
state.recipe = null
return
state.recipe = null;
return;
}
if ("nutrition" in rec && rec.nutrition instanceof Array) {
rec.nutrition = {}
if ('nutrition' in rec && rec.nutrition instanceof Array) {
rec.nutrition = {};
}
state.recipe = rec
state.recipe = rec;
// Setting recipe also means that loading/reloading the recipe has finished
state.loadingRecipe = 0
state.reloadingRecipe = 0
state.loadingRecipe = 0;
state.reloadingRecipe = 0;
},
setRecipeCategory(state, { c }) {
state.recipe.category = c
state.recipe.category = c;
},
setRecipeFilters(state, { f }) {
state.recipeFilters = f;
},
setReloadingRecipe(state, { r }) {
state.reloadingRecipe = r
state.reloadingRecipe = r;
},
setSavingRecipe(state, { b }) {
state.savingRecipe = b
state.savingRecipe = b;
},
setShowTagCloudInRecipeList(state, { b }) {
localStorage.setItem("showTagCloudInRecipeList", JSON.stringify(b))
state.localSettings.showTagCloudInRecipeList = b
localStorage.setItem('showTagCloudInRecipeList', JSON.stringify(b));
state.localSettings.showTagCloudInRecipeList = b;
},
setUser(state, { u }) {
state.user = u
state.user = u;
},
setUpdatingRecipeDirectory(state, { b }) {
state.updatingRecipeDirectory = b
state.updatingRecipeDirectory = b;
},
},
@ -123,8 +123,8 @@ var store = new Vuex.Store({
* Read/Update the user settings from the backend
*/
async refreshConfig(c) {
const config = (await api.config.get()).data
c.commit("setConfig", { config })
const config = (await api.config.get()).data;
c.commit('setConfig', { config });
},
/*
@ -138,114 +138,117 @@ var store = new Vuex.Store({
* Create new recipe on the server
*/
createRecipe(c, { recipe }) {
const request = api.recipes.create(recipe)
const request = api.recipes.create(recipe);
return request.then((v) => {
// Refresh navigation to display changes
c.dispatch("setAppNavigationRefreshRequired", {
c.dispatch('setAppNavigationRefreshRequired', {
isRequired: true,
})
});
return v
})
return v;
});
},
/**
* Delete recipe on the server
*/
deleteRecipe(c, { id }) {
const request = api.recipes.delete(id)
const request = api.recipes.delete(id);
request.then(() => {
// Refresh navigation to display changes
c.dispatch("setAppNavigationRefreshRequired", {
c.dispatch('setAppNavigationRefreshRequired', {
isRequired: true,
})
})
return request
});
});
return request;
},
setAppNavigationVisible(c, { isVisible }) {
c.commit("setAppNavigationVisible", { b: isVisible })
c.commit('setAppNavigationVisible', { b: isVisible });
},
setAppNavigationRefreshRequired(c, { isRequired }) {
c.commit("setAppNavigationRefreshRequired", { b: isRequired })
c.commit('setAppNavigationRefreshRequired', { b: isRequired });
},
setLoadingRecipe(c, { recipe }) {
c.commit("setLoadingRecipe", { r: parseInt(recipe, 10) })
c.commit('setLoadingRecipe', { r: parseInt(recipe, 10) });
},
setPage(c, { page }) {
c.commit("setPage", { p: page })
c.commit('setPage', { p: page });
},
setRecipe(c, { recipe }) {
c.commit("setRecipe", { r: recipe })
c.commit('setRecipe', { r: recipe });
},
setRecipeFilters(c, filters) {
c.commit("setRecipeFilters", { f: filters })
c.commit('setRecipeFilters', { f: filters });
},
setReloadingRecipe(c, { recipe }) {
c.commit("setReloadingRecipe", { r: parseInt(recipe, 10) })
c.commit('setReloadingRecipe', { r: parseInt(recipe, 10) });
},
setSavingRecipe(c, { saving }) {
c.commit("setSavingRecipe", { b: saving })
c.commit('setSavingRecipe', { b: saving });
},
setUser(c, { user }) {
c.commit("setUser", { u: user })
c.commit('setUser', { u: user });
},
setCategoryUpdating(c, { category }) {
c.commit("setCategoryUpdating", { c: category })
c.commit('setCategoryUpdating', { c: category });
},
setShowTagCloudInRecipeList(c, { showTagCloud }) {
c.commit("setShowTagCloudInRecipeList", { b: showTagCloud })
c.commit('setShowTagCloudInRecipeList', { b: showTagCloud });
},
updateCategoryName(c, { categoryNames }) {
const oldName = categoryNames[0]
const newName = categoryNames[1]
c.dispatch("setCategoryUpdating", { category: oldName })
const oldName = categoryNames[0];
const newName = categoryNames[1];
c.dispatch('setCategoryUpdating', { category: oldName });
const request = api.categories.update(oldName, newName)
const request = api.categories.update(oldName, newName);
request
.then(() => {
if (c.state.recipe && c.state.recipe.recipeCategory === oldName) {
c.commit("setRecipeCategory", { c: newName })
if (
c.state.recipe &&
c.state.recipe.recipeCategory === oldName
) {
c.commit('setRecipeCategory', { c: newName });
}
})
.catch((e) => {
if (e && e instanceof Error) {
throw e
throw e;
}
})
.then(() => {
// finally
c.dispatch("setCategoryUpdating", { category: null })
})
c.dispatch('setCategoryUpdating', { category: null });
});
return request
return request;
},
updateRecipeDirectory(c, { dir }) {
c.commit("setUpdatingRecipeDirectory", { b: true })
c.dispatch("setRecipe", { recipe: null })
const request = api.config.directory.update(dir)
c.commit('setUpdatingRecipeDirectory', { b: true });
c.dispatch('setRecipe', { recipe: null });
const request = api.config.directory.update(dir);
request.then(() => {
c.dispatch("setAppNavigationRefreshRequired", {
c.dispatch('setAppNavigationRefreshRequired', {
isRequired: true,
})
c.commit("setUpdatingRecipeDirectory", { b: false })
})
return request
});
c.commit('setUpdatingRecipeDirectory', { b: false });
});
return request;
},
/**
* Update existing recipe on the server
*/
updateRecipe(c, { recipe }) {
const request = api.recipes.update(recipe.id, recipe)
const request = api.recipes.update(recipe.id, recipe);
request.then(() => {
// Refresh navigation to display changes
c.dispatch("setAppNavigationRefreshRequired", {
c.dispatch('setAppNavigationRefreshRequired', {
isRequired: true,
})
})
return request
});
});
return request;
},
},
})
});
export const useStore = () => store;

Просмотреть файл

@ -3,7 +3,7 @@ const stylelintConfig = require('@nextcloud/stylelint-config')
stylelintConfig.extends.push('stylelint-config-idiomatic-order')
stylelintConfig.rules.indentation = null
stylelintConfig.rules['string-quotes'] = 'double'
stylelintConfig.rules['string-quotes'] = 'single'
stylelintConfig.rules['function-no-unknown'] = [true, {
'ignoreFunctions': ["math.div"]
}]