mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
ux: unify UI based on user-feedback (#1216)
* unify UI based on user-feedback * fix layout shify error * implement drag and drop animation
This commit is contained in:
parent
8f1c082d79
commit
4fe19b88ca
@ -24,19 +24,21 @@
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</RecipeCardImage>
|
||||
<v-card-title class="my-n3 mb-n6">
|
||||
<v-card-title class="my-n3 px-2 mb-n6">
|
||||
<div class="headerClass">
|
||||
{{ name }}
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<slot name="actions">
|
||||
<v-card-actions>
|
||||
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
|
||||
<RecipeRating :value="rating" :name="name" :slug="slug" :small="true" />
|
||||
<v-card-actions class="px-1">
|
||||
<RecipeFavoriteBadge v-if="loggedIn" class="absolute" :slug="slug" show-always />
|
||||
|
||||
<RecipeRating class="pb-1" :value="rating" :name="name" :slug="slug" :small="true" />
|
||||
<v-spacer></v-spacer>
|
||||
<RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" url-prefix="tags" />
|
||||
<RecipeContextMenu
|
||||
color="grey darken-2"
|
||||
:slug="slug"
|
||||
:name="name"
|
||||
:recipe-id="recipeId"
|
||||
|
@ -40,6 +40,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
|
||||
import colors from "vuetify/lib/util/colors";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
export interface ContextMenuIncludes {
|
||||
@ -91,7 +92,7 @@ export default defineComponent({
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
default: colors.grey.darken2,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
|
@ -8,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-app-bar dark color="primary" class="mt-n1 mb-3">
|
||||
<v-app-bar dense dark color="primary" class="mb-2">
|
||||
<v-icon large left>
|
||||
{{ $globals.icons.createAlt }}
|
||||
</v-icon>
|
||||
@ -21,34 +21,26 @@
|
||||
v-model="inputText"
|
||||
outlined
|
||||
rows="12"
|
||||
hide-details
|
||||
:placeholder="$t('new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list')"
|
||||
>
|
||||
</v-textarea>
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn outlined color="info" small v-bind="attrs" @click="trimAllLines" v-on="on">
|
||||
Trim Whitespace
|
||||
</v-btn>
|
||||
</template>
|
||||
<span> Trim leading and trailing whitespace as well as blank lines </span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn class="ml-1" outlined color="info" small v-bind="attrs" @click="removeFirstCharacter" v-on="on">
|
||||
Trim Prefix
|
||||
</v-btn>
|
||||
<v-divider></v-divider>
|
||||
<template v-for="(util, idx) in utilities">
|
||||
<v-list-item :key="util.id" dense class="py-1">
|
||||
<v-list-item-title>
|
||||
<v-list-item-subtitle class="wrap-word">
|
||||
{{ util.description }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-title>
|
||||
<BaseButton small color="info" @click="util.action">
|
||||
<template #icon> {{ $globals.icons.robot }}</template>
|
||||
Run
|
||||
</BaseButton>
|
||||
</v-list-item>
|
||||
<v-divider :key="`divider-${idx}`" class="mx-2"></v-divider>
|
||||
</template>
|
||||
<span> Trim first character from each line </span>
|
||||
</v-tooltip>
|
||||
<v-tooltip top>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn class="ml-1" outlined color="info" small v-bind="attrs" @click="splitByNumberedLine" v-on="on">
|
||||
Split By Numbered Line
|
||||
</v-btn>
|
||||
</template>
|
||||
<span> Attempts to split a paragraph by matching 1) or 1. patterns </span>
|
||||
</v-tooltip>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
@ -64,7 +56,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { reactive, toRefs, defineComponent } from "@nuxtjs/composition-api";
|
||||
import { reactive, toRefs, defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
export default defineComponent({
|
||||
setup(_, context) {
|
||||
const state = reactive({
|
||||
@ -78,7 +70,7 @@ export default defineComponent({
|
||||
|
||||
function removeFirstCharacter() {
|
||||
state.inputText = splitText()
|
||||
.map((line) => line.substr(1))
|
||||
.map((line) => line.substring(1))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
@ -109,7 +101,28 @@ export default defineComponent({
|
||||
state.dialog = false;
|
||||
}
|
||||
|
||||
const { i18n } = useContext();
|
||||
|
||||
const utilities = [
|
||||
{
|
||||
id: "trim-whitespace",
|
||||
description: i18n.tc("new-recipe.trim-whitespace-description"),
|
||||
action: trimAllLines,
|
||||
},
|
||||
{
|
||||
id: "trim-prefix",
|
||||
description: i18n.tc("new-recipe.trim-prefix-description"),
|
||||
action: removeFirstCharacter,
|
||||
},
|
||||
{
|
||||
id: "split-by-numbered-line",
|
||||
description: i18n.tc("new-recipe.split-by-numbered-line-description"),
|
||||
action: splitByNumberedLine,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
utilities,
|
||||
splitText,
|
||||
trimAllLines,
|
||||
removeFirstCharacter,
|
||||
|
@ -21,18 +21,12 @@
|
||||
type="number"
|
||||
placeholder="Quantity"
|
||||
>
|
||||
<v-icon
|
||||
v-if="$listeners && $listeners.delete"
|
||||
slot="prepend"
|
||||
class="mr-n1"
|
||||
color="error"
|
||||
@click="$emit('delete')"
|
||||
>
|
||||
{{ $globals.icons.delete }}
|
||||
<v-icon v-if="$listeners && $listeners.delete" slot="prepend" class="mr-n1 handle">
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
</v-icon>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col v-if="!disableAmount && units" sm="12" md="3" cols="12">
|
||||
<v-col v-if="!disableAmount" sm="12" md="3" cols="12">
|
||||
<v-autocomplete
|
||||
v-model="value.unit"
|
||||
:search-input.sync="unitSearch"
|
||||
@ -40,7 +34,7 @@
|
||||
dense
|
||||
solo
|
||||
return-object
|
||||
:items="units"
|
||||
:items="units || []"
|
||||
item-text="name"
|
||||
class="mx-1"
|
||||
placeholder="Choose Unit"
|
||||
@ -59,7 +53,7 @@
|
||||
</v-col>
|
||||
|
||||
<!-- Foods Input -->
|
||||
<v-col v-if="!disableAmount && foods" m="12" md="3" cols="12" class="">
|
||||
<v-col v-if="!disableAmount" m="12" md="3" cols="12" class="">
|
||||
<v-autocomplete
|
||||
v-model="value.food"
|
||||
:search-input.sync="foodSearch"
|
||||
@ -67,7 +61,7 @@
|
||||
dense
|
||||
solo
|
||||
return-object
|
||||
:items="foods"
|
||||
:items="foods || []"
|
||||
item-text="name"
|
||||
class="mx-1 py-0"
|
||||
placeholder="Choose Food"
|
||||
@ -85,28 +79,34 @@
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
<v-col sm="12" md="" cols="12">
|
||||
<div class="d-flex">
|
||||
<v-text-field v-model="value.note" hide-details dense solo class="mx-1" placeholder="Notes">
|
||||
<v-icon v-if="disableAmount" slot="prepend" class="mr-n1" color="error" @click="$emit('delete')">
|
||||
{{ $globals.icons.delete }}
|
||||
<v-icon v-if="disableAmount && $listeners && $listeners.delete" slot="prepend" class="mr-n1 handle">
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
</v-icon>
|
||||
|
||||
<template slot="append-outer">
|
||||
</v-text-field>
|
||||
<BaseButtonGroup
|
||||
hover
|
||||
:large="false"
|
||||
class="handle my-auto"
|
||||
class="my-auto"
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.arrowUpDown,
|
||||
text: '',
|
||||
icon: $globals.icons.delete,
|
||||
text: 'Delete',
|
||||
event: 'delete',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.dotsVertical,
|
||||
text: 'Menu',
|
||||
event: 'open',
|
||||
children: contextMenuOptions,
|
||||
},
|
||||
]"
|
||||
@toggle-section="toggleTitle"
|
||||
@toggle-original="toggleOriginalText"
|
||||
@delete="$emit('delete')"
|
||||
/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<p v-if="showOriginalText" class="text-caption">Original Text: {{ value.originalText }}</p>
|
||||
|
@ -71,11 +71,17 @@
|
||||
:disabled="!edit"
|
||||
:value="value"
|
||||
handle=".handle"
|
||||
v-bind="{
|
||||
animation: 200,
|
||||
group: 'description',
|
||||
ghostClass: 'ghost',
|
||||
}"
|
||||
@input="updateIndex"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
>
|
||||
<div v-for="(step, index) in value" :key="step.id">
|
||||
<TransitionGroup type="transition" :name="!drag ? 'flip-list' : ''">
|
||||
<div v-for="(step, index) in value" :key="step.id" class="list-group-item">
|
||||
<v-app-bar
|
||||
v-if="showTitleEditor[step.id]"
|
||||
class="primary mx-1 mt-6"
|
||||
@ -109,17 +115,20 @@
|
||||
@click="toggleDisabled(index)"
|
||||
>
|
||||
<v-card-title :class="{ 'pb-0': !isChecked(index) }">
|
||||
<v-btn v-if="edit" fab x-small color="white" class="mr-2" elevation="0" @click="value.splice(index, 1)">
|
||||
<v-icon size="24" color="error">{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<span class="handle">
|
||||
<v-icon v-if="edit" size="26" class="pb-1">{{ $globals.icons.arrowUpDown }}</v-icon>
|
||||
{{ $t("recipe.step-index", { step: index + 1 }) }}
|
||||
|
||||
</span>
|
||||
<template v-if="edit">
|
||||
<v-icon class="handle ml-auto mr-2">{{ $globals.icons.arrowUpDown }}</v-icon>
|
||||
<div>
|
||||
<div class="ml-auto">
|
||||
<BaseButtonGroup
|
||||
:large="false"
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $tc('general.delete'),
|
||||
event: 'delete',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.dotsVertical,
|
||||
text: '',
|
||||
@ -137,22 +146,22 @@
|
||||
text: 'Merge Above',
|
||||
event: 'merge-above',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: previewStates[index] ? $globals.icons.edit : $globals.icons.eye,
|
||||
text: previewStates[index] ? $tc('general.edit') : 'Preview Markdown',
|
||||
text: previewStates[index] ? 'Edit Markdown' : 'Preview Markdown',
|
||||
event: 'preview-step',
|
||||
},
|
||||
],
|
||||
},
|
||||
]"
|
||||
@merge-above="mergeAbove(index - 1, index)"
|
||||
@toggle-section="toggleShowTitle(step.id)"
|
||||
@link-ingredients="openDialog(index, step.ingredientReferences, step.text)"
|
||||
@preview-step="togglePreviewState(index)"
|
||||
@delete="value.splice(index, 1)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-fade-transition>
|
||||
<v-icon v-show="isChecked(index)" size="24" class="ml-auto" color="success">
|
||||
{{ $globals.icons.checkboxMarkedCircle }}
|
||||
@ -181,6 +190,7 @@
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</draggable>
|
||||
</section>
|
||||
</template>
|
||||
@ -481,7 +491,10 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
const drag = ref(false);
|
||||
|
||||
return {
|
||||
drag,
|
||||
togglePreviewState,
|
||||
toggleCollapseSection,
|
||||
previewStates,
|
||||
@ -521,4 +534,23 @@ export default defineComponent({
|
||||
.markdown >>> ol > li {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
.flip-list-move {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
.no-move {
|
||||
transition: transform 0s;
|
||||
}
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.list-group {
|
||||
min-height: 38px;
|
||||
}
|
||||
.list-group-item {
|
||||
cursor: move;
|
||||
}
|
||||
.list-group-item i {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,29 +1,27 @@
|
||||
<template>
|
||||
<div v-if="value.length > 0 || edit">
|
||||
<div v-if="value.length > 0 || edit" class="mt-8">
|
||||
<h2 class="my-4">{{ $t("recipe.note") }}</h2>
|
||||
<v-card v-for="(note, index) in value" :key="'note' + index" class="mt-1">
|
||||
<div v-if="edit">
|
||||
<div v-for="(note, index) in value" :key="'note' + index" class="mt-1">
|
||||
<v-card v-if="edit">
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-btn fab x-small color="white" class="mr-2" elevation="0" @click="removeByIndex(value, index)">
|
||||
<v-icon color="error">{{ $globals.icons.delete }}</v-icon>
|
||||
<div class="d-flex align-center">
|
||||
<v-text-field v-model="value[index]['title']" :label="$t('recipe.title')" />
|
||||
<v-btn icon class="mr-2" elevation="0" @click="removeByIndex(value, index)">
|
||||
<v-icon>{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
<v-text-field v-model="value[index]['title']" :label="$t('recipe.title')"></v-text-field>
|
||||
</v-row>
|
||||
|
||||
<v-textarea v-model="value[index]['text']" auto-grow :placeholder="$t('recipe.note')"> </v-textarea>
|
||||
</v-card-text>
|
||||
</div>
|
||||
<v-textarea v-model="value[index]['text']" auto-grow :placeholder="$t('recipe.note')" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div v-else>
|
||||
<v-card-title class="py-2">
|
||||
<v-card-title class="text-subtitle-1 font-weight-medium py-1">
|
||||
{{ note.title }}
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<VueMarkdown :source="note.text"> </VueMarkdown>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div v-if="edit" class="d-flex justify-end">
|
||||
<BaseButton class="ml-auto my-2" @click="addNote"> {{ $t("general.new") }}</BaseButton>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-item-group>
|
||||
<template v-for="btn in buttons">
|
||||
<v-menu v-if="btn.children" :key="'menu-' + btn.event" active-class="pa-0" offset-x left>
|
||||
<v-menu v-if="btn.children" :key="'menu-' + btn.event" active-class="pa-0" offset-y top left>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn tile :large="large" icon v-bind="attrs" v-on="on">
|
||||
<v-icon>
|
||||
|
@ -215,7 +215,10 @@
|
||||
"upload-a-recipe": "Upload a Recipe",
|
||||
"upload-individual-zip-file": "Upload an individual .zip file exported from another Mealie instance.",
|
||||
"url-form-hint": "Copy and paste a link from your favorite recipe website",
|
||||
"view-scraped-data": "View Scraped Data"
|
||||
"view-scraped-data": "View Scraped Data",
|
||||
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
|
||||
"trim-prefix-description": "Trim first character from each line",
|
||||
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns"
|
||||
},
|
||||
"page": {
|
||||
"404-page-not-found": "404 Page not found",
|
||||
|
@ -127,14 +127,29 @@
|
||||
<!-- Advanced Editor -->
|
||||
<div v-if="form">
|
||||
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
|
||||
<draggable v-if="recipe.recipeIngredient.length > 0" v-model="recipe.recipeIngredient" handle=".handle">
|
||||
<draggable
|
||||
v-if="recipe.recipeIngredient.length > 0"
|
||||
v-model="recipe.recipeIngredient"
|
||||
handle=".handle"
|
||||
v-bind="{
|
||||
animation: 200,
|
||||
group: 'description',
|
||||
disabled: false,
|
||||
ghostClass: 'ghost',
|
||||
}"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
>
|
||||
<TransitionGroup type="transition" :name="!drag ? 'flip-list' : ''">
|
||||
<RecipeIngredientEditor
|
||||
v-for="(ingredient, index) in recipe.recipeIngredient"
|
||||
:key="ingredient.referenceId"
|
||||
v-model="recipe.recipeIngredient[index]"
|
||||
class="list-group-item"
|
||||
:disable-amount="recipe.settings.disableAmount"
|
||||
@delete="recipe.recipeIngredient.splice(index, 1)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</draggable>
|
||||
<v-skeleton-loader v-else boilerplate elevation="2" type="list-item"> </v-skeleton-loader>
|
||||
<div class="d-flex justify-end mt-2">
|
||||
@ -820,8 +835,11 @@ export default defineComponent({
|
||||
return "Parse ingredients";
|
||||
});
|
||||
|
||||
const drag = ref(false);
|
||||
|
||||
return {
|
||||
// Wake Lock
|
||||
drag,
|
||||
wakeIsSupported,
|
||||
isActive,
|
||||
lockScreen,
|
||||
@ -857,3 +875,24 @@ export default defineComponent({
|
||||
head: {},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.flip-list-move {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
.no-move {
|
||||
transition: transform 0s;
|
||||
}
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.list-group {
|
||||
min-height: 38px;
|
||||
}
|
||||
.list-group-item {
|
||||
cursor: move;
|
||||
}
|
||||
.list-group-item i {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user