Merge remote-tracking branch 'origin/main' into main

This commit is contained in:
Hosted Weblate 2021-04-14 11:28:49 +02:00
commit bb02c1779c
32 changed files with 4482 additions and 17848 deletions

View file

@ -15,17 +15,18 @@ Page {
font-family: "Inter-Medium"; font-family: "Inter-Medium";
font-size: 14; font-size: 14;
} }
.tb {
font-family: "Inter-Bold";
}
.ico { .ico {
font-family: "enrecipes"; font-family: "enrecipes";
font-size: 24; font-size: 24;
vertical-alignment: center; vertical-alignment: center;
&.sm { &.sm {
font-size: 16; font-size: 16;
opacity: 0.5;
} }
} }
.tb {
font-family: "Inter-Bold";
}
.tac { .tac {
text-align: center; text-align: center;
} }
@ -42,7 +43,8 @@ Page {
color: $gray9; color: $gray9;
background: $gray1; background: $gray1;
Page, Page,
.filters { .filters,
.sticky {
background: $gray1; background: $gray1;
} }
TextField, TextField,
@ -54,8 +56,7 @@ Page {
border-color: $gray2; border-color: $gray2;
} }
.appbar, .appbar,
.modal, .modal {
.topPlate {
background: $gray0; background: $gray0;
} }
.fieldLabel, .fieldLabel,
@ -71,19 +72,16 @@ Page {
color: $gray1; color: $gray1;
background: $gray9; background: $gray9;
} }
Progress { .fab {
progress-background-color: $gray4; color: #fff;
}
.fab,
.hlMsg {
color: $gray1;
} }
} }
.Dark { .Dark {
color: $gray1; color: $gray1;
background: $gray9; background: $gray9;
Page, Page,
.filters { .filters,
.sticky {
background: $gray9; background: $gray9;
} }
TextField, TextField,
@ -95,8 +93,7 @@ Page {
border-color: $gray7; border-color: $gray7;
} }
.appbar, .appbar,
.modal, .modal {
.topPlate {
color: $gray0; color: $gray0;
background: $gray8; background: $gray8;
} }
@ -113,18 +110,16 @@ Page {
color: $gray9; color: $gray9;
background: $gray1; background: $gray1;
} }
Progress { .fab {
progress-background-color: $gray5;
}
.fab,
.hlMsg {
color: $gray9; color: $gray9;
} }
} }
.Black { .Black {
color: $gray2; color: $gray2;
background: $gray10; background: $gray10;
Page { Page,
.filters,
.sticky {
background: $gray10; background: $gray10;
} }
TextField, TextField,
@ -136,8 +131,7 @@ Page {
border-color: $gray8; border-color: $gray8;
} }
.appbar, .appbar,
.modal, .modal {
.topPlate {
color: $gray1; color: $gray1;
background: $gray9; background: $gray9;
} }
@ -154,11 +148,7 @@ Page {
color: $gray10; color: $gray10;
background: $gray2; background: $gray2;
} }
Progress { .fab {
progress-background-color: $gray5;
}
.fab,
.hlMsg {
color: $gray10; color: $gray10;
} }
} }
@ -191,7 +181,8 @@ TextView {
width: 100%; width: 100%;
} }
Progress { Progress {
progress-color: $orange; margin: 16;
background-color: $gray5;
} }
Switch { Switch {
background-color: $orange; background-color: $orange;
@ -200,7 +191,7 @@ Switch {
// ----------------------------- // -----------------------------
// Side Drawer // Side Drawer
.segment { .segment {
border-radius: 8; border-radius: 12;
margin: 0 4 0 0; margin: 0 4 0 0;
padding: 0 12; padding: 0 12;
.value { .value {
@ -216,7 +207,7 @@ Button {
background-color: transparent; background-color: transparent;
z-index: 0; z-index: 0;
padding: 8; padding: 8;
border-radius: 8; border-radius: 12;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
&:active { &:active {
@ -276,7 +267,7 @@ Button {
padding: 8 16; padding: 8 16;
.recipeInfo { .recipeInfo {
vertical-alignment: top; vertical-alignment: top;
padding: 4 0 4 8; padding: 2 0 2 8;
} }
.title { .title {
padding: 0 0 4; padding: 0 0 4;
@ -318,7 +309,7 @@ Button {
background-color: transparent; background-color: transparent;
} }
.imgHolder { .imgHolder {
border-radius: 8; border-radius: 12;
} }
// ----------------------------- // -----------------------------
// SETTINGS // SETTINGS
@ -328,6 +319,7 @@ Button {
} }
.options-list { .options-list {
.option { .option {
vertical-align: center;
padding: 14 8; padding: 14 8;
margin: 0 16; margin: 0 16;
.ico { .ico {
@ -353,7 +345,7 @@ Button {
.icon { .icon {
margin: 16; margin: 16;
background: $orange; background: $orange;
border-radius: 99; border-radius: 16;
horizontal-alignment: center; horizontal-alignment: center;
} }
.name { .name {
@ -367,16 +359,21 @@ Button {
// ----------------------------- // -----------------------------
// VIEW RECIPE // VIEW RECIPE
.photo { .photo {
border-radius: 8; border-radius: 12;
margin: 24 16 0 0; margin: 24 16 0 0;
vertical-align: top; vertical-align: top;
} }
.photoviewer {
width: 96;
height: 96;
opacity: 0;
background: #000;
}
.attribute { .attribute {
margin: 8 16; margin: 8 16;
.title { .title {
margin-right: 8; margin-right: 8;
font-size: 12; font-size: 12;
color: $gray6;
} }
.value { .value {
@extend .tb; @extend .tb;
@ -425,18 +422,19 @@ Button {
line-height: 4; line-height: 4;
padding: 16 0; padding: 16 0;
} }
.dateInfo {
padding: 32 16 16;
font-size: 12;
line-height: 4;
}
// ----------------------------- // -----------------------------
// APPBAR // APPBAR
.appbar { .appbar {
z-index: 99; z-index: 32;
min-height: 56; min-height: 56;
margin: 8; margin: 8;
padding: 4; padding: 4;
border-radius: 10; border-radius: 16;
elevation: 16;
&.hlMsg {
background: $orange;
}
.title { .title {
@extend .tb; @extend .tb;
@extend .tw; @extend .tw;
@ -451,7 +449,7 @@ Button {
} }
} }
.toolbar { .toolbar {
z-index: 98; z-index: 24;
padding: 4; padding: 4;
margin-bottom: 0; margin-bottom: 0;
horizontal-alignment: left; horizontal-alignment: left;
@ -469,7 +467,7 @@ Button {
width: 48; width: 48;
height: 48; height: 48;
margin: 0 4 0 0; margin: 0 4 0 0;
border-radius: 8; border-radius: 12;
background: $orange; background: $orange;
} }
// ----------------------------- // -----------------------------
@ -481,11 +479,12 @@ Button {
padding: 0; padding: 0;
margin: 32 0 16; margin: 32 0 16;
} }
.sticky {
width: 100%;
padding: 0 16 16;
margin: 0;
}
.countdown { .countdown {
background-color: transparent;
width: 48;
height: 48;
z-index: 0;
font-size: 17; font-size: 17;
color: $orange; color: $orange;
} }
@ -507,7 +506,7 @@ Button {
text-align: center; text-align: center;
} }
.day { .day {
border-radius: 8; border-radius: 12;
} }
.hasPlans { .hasPlans {
color: $orange; color: $orange;
@ -536,7 +535,7 @@ Button {
.modal { .modal {
max-width: 320; max-width: 320;
width: 100%; width: 100%;
border-radius: 10; border-radius: 12;
margin: 72 0; margin: 72 0;
.title { .title {
@extend .tb; @extend .tb;
@ -566,7 +565,7 @@ Button {
background: transparent; background: transparent;
} }
.shareItem { .shareItem {
border-radius: 8; border-radius: 12;
margin: 0 8 8; margin: 0 8 8;
text-align: center; text-align: center;
.ico { .ico {
@ -624,6 +623,7 @@ ActivityIndicator {
} }
@keyframes fade { @keyframes fade {
0% { 0% {
opacity: 1;
} }
100% { 100% {
opacity: 0.5; opacity: 0.5;

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@
for="recipe in getList" for="recipe in getList"
@loaded="onListLoad" @loaded="onListLoad"
:itemTemplateSelector="getLayout" :itemTemplateSelector="getLayout"
:colWidth="layout == 'grid' ? '50%' : '100%'" :colWidth="layout == 'grid' || layout == 'photogrid' ? '50%' : '100%'"
@scroll="!selectMode && onScroll($event)" @scroll="!selectMode && onScroll($event)"
> >
<v-template name="header"> <v-template name="header">
@ -31,20 +31,20 @@
v-for="(item, index) in topmenu" v-for="(item, index) in topmenu"
:key="index" :key="index"
:class="{ :class="{
select: currentComponent === item.component, select: currentComponent === item.title,
}" }"
@touch="touchSelector($event, item.component)" @touch="touchSelector($event, item.title, item.title)"
> >
<Label class="ico" :text="icon[item.icon]" /> <Label class="ico" :text="icon[item.icon]" />
<Label <Label
class="value" class="value"
v-if="getRecipeCount(item.title)" :hidden="!getRecipeCount(item.title)"
:text="getRecipeCount(item.title)" :text="getRecipeCount(item.title)"
col="1" col="1"
/> />
</GridLayout> </GridLayout>
<GridLayout <GridLayout
v-if="currentComponent === 'Filtered recipes'" :hidden="currentComponent !== 'Filtered recipes'"
rows="48" rows="48"
columns="auto, auto" columns="auto, auto"
class="segment" class="segment"
@ -52,7 +52,7 @@
select: currentComponent === 'Filtered recipes', select: currentComponent === 'Filtered recipes',
}" }"
> >
<Label class="ico" :text="icon.sort" /> <Label class="ico" :text="icon.filter" />
<Label <Label
class="value" class="value"
:text="getRecipeCount('filtered')" :text="getRecipeCount('filtered')"
@ -65,7 +65,7 @@
<GridLayout <GridLayout
class="recipeItem" class="recipeItem"
:class="getItemPos(recipe.id)" :class="getItemPos(recipe.id)"
rows="96" rows="auto"
columns="96, *" columns="96, *"
ref="recipe" ref="recipe"
@longPress=" @longPress="
@ -77,9 +77,10 @@
> >
<Image <Image
class="imgHolder" class="imgHolder"
verticalAlignment="top"
v-if="recipe.imageSrc" v-if="recipe.imageSrc"
:src="recipe.imageSrc" :src="recipe.imageSrc"
stretch="aspectFit" stretch="none"
decodeWidth="96" decodeWidth="96"
decodeHeight="96" decodeHeight="96"
loadMode="async" loadMode="async"
@ -87,6 +88,7 @@
<Label <Label
v-else v-else
class="ico imgHolder" class="ico imgHolder"
verticalAlignment="top"
@loaded="centerLabel" @loaded="centerLabel"
width="96" width="96"
height="96" height="96"
@ -94,15 +96,15 @@
:text="icon.img" :text="icon.img"
/> />
<StackLayout class="recipeInfo" col="1"> <StackLayout class="recipeInfo" col="1">
<Label :text="recipe.title" class="tb title" /> <Label :text="recipe.title" class="tb title tw" />
<StackLayout class="attributes" orientation="horizontal"> <StackLayout class="attributes" orientation="horizontal"
<Label class="ico sm" :text="icon.cuisine" /> ><Label class="ico sm" :text="icon.cuisine" />
<Label class="attr" :text="recipe.cuisine | L" /> <Label class="attr" :text="recipe.cuisine | L" />
<Label class="ico sm" :text="icon.category" /> <Label class="ico sm" :text="icon.category" />
<Label class="attr" :text="recipe.category | L" /> <Label class="attr" :text="recipe.category | L" />
</StackLayout> </StackLayout>
<StackLayout <StackLayout
v-if="recipe.tags.length" :hidden="!recipe.tags.length"
class="attributes" class="attributes"
orientation="horizontal" orientation="horizontal"
> >
@ -158,7 +160,7 @@
:text="icon.img" :text="icon.img"
/> />
<StackLayout class="recipeInfo" row="1"> <StackLayout class="recipeInfo" row="1">
<Label :text="recipe.title" class="tb title" /> <Label :text="recipe.title" class="tb title tw" />
<StackLayout class="attributes" orientation="horizontal"> <StackLayout class="attributes" orientation="horizontal">
<Label class="ico sm" :text="icon.cuisine" /> <Label class="ico sm" :text="icon.cuisine" />
<Label class="attr" :text="recipe.cuisine | L" /> <Label class="attr" :text="recipe.cuisine | L" />
@ -168,7 +170,7 @@
<Label class="attr" :text="recipe.category | L" /> <Label class="attr" :text="recipe.category | L" />
</StackLayout> </StackLayout>
<StackLayout <StackLayout
v-if="recipe.tags.length" :hidden="!recipe.tags.length"
class="attributes" class="attributes"
orientation="horizontal" orientation="horizontal"
> >
@ -178,6 +180,43 @@
</StackLayout> </StackLayout>
</GridLayout> </GridLayout>
</v-template> </v-template>
<v-template name="photogrid">
<GridLayout
class="recipeItem grid"
:class="getItemPos(recipe.id)"
rows="auto, auto"
columns="*"
ref="recipe"
@longPress="
selectMode ? viewRecipe(recipe.id) : addToSelection(recipe.id)
"
@tap="
selectMode ? addToSelection(recipe.id) : viewRecipe(recipe.id)
"
>
<Image
class="imgHolder"
v-if="recipe.imageSrc"
:src="recipe.imageSrc"
stretch="aspectFit"
:decodeWidth="imgWidth"
:decodeHeight="imgWidth"
loadMode="async"
/>
<Label
v-else
width="100%"
:height="imgWidth"
@loaded="centerLabel"
class="ico imgHolder"
:fontSize="imgWidth / 2"
:text="icon.img"
/>
<StackLayout class="recipeInfo" row="1">
<Label :text="recipe.title" class="tb title tw" />
</StackLayout>
</GridLayout>
</v-template>
<v-template name="simple"> <v-template name="simple">
<GridLayout <GridLayout
class="recipeItem simple" class="recipeItem simple"
@ -192,7 +231,7 @@
" "
> >
<StackLayout class="recipeInfo"> <StackLayout class="recipeInfo">
<Label :text="recipe.title" class="tb title" /> <Label :text="recipe.title" class="tb title tw" />
<StackLayout class="attributes" orientation="horizontal"> <StackLayout class="attributes" orientation="horizontal">
<Label class="ico sm" :text="icon.cuisine" /> <Label class="ico sm" :text="icon.cuisine" />
<Label class="attr" :text="recipe.cuisine | L" /> <Label class="attr" :text="recipe.cuisine | L" />
@ -200,7 +239,7 @@
<Label class="attr" :text="recipe.category | L" /> <Label class="attr" :text="recipe.category | L" />
</StackLayout> </StackLayout>
<StackLayout <StackLayout
v-if="recipe.tags.length" :hidden="!recipe.tags.length"
class="attributes" class="attributes"
orientation="horizontal" orientation="horizontal"
> >
@ -224,7 +263,7 @@
" "
> >
<StackLayout class="recipeInfo"> <StackLayout class="recipeInfo">
<Label :text="recipe.title" class="tb title" /> <Label :text="recipe.title" class="tb title tw" />
</StackLayout> </StackLayout>
</GridLayout> </GridLayout>
</v-template> </v-template>
@ -273,7 +312,7 @@
rows="auto" rows="auto"
columns="auto" columns="auto"
class="appbar toolbar" class="appbar toolbar"
v-if="showTools" :hidden="!showTools"
> >
<GridLayout <GridLayout
row="1" row="1"
@ -319,41 +358,34 @@
@textChange="updateList($event.value)" @textChange="updateList($event.value)"
/> />
<Label <Label
v-if="selectMode" :hidden="!selectMode"
class="title" class="title"
:text="`${selection.length} ${$options.filters.L('sltd')}`" :text="`${selection.length} ${$options.filters.L('sltd')}`"
col="1" col="1"
/> />
<Button <StackLayout
v-if="recipes.length && !selectMode && !showSearch"
class="ico"
:text="selectMode ? icon.export : icon.sear"
col="2" col="2"
@tap="selectMode ? exportSelection() : openSearch()" colSpan="3"
/> orientation="horizontal"
:hidden="!recipes.length || selectMode || showSearch"
>
<Button
class="ico"
:text="selectMode ? icon.exp : icon.sear"
@tap="selectMode ? exportSelection() : openSearch()"
/>
<Button class="ico" :text="icon.sort" @tap="openSort" />
<Button class="ico" :text="icon.filter" @tap="openFilters" />
</StackLayout>
<Button <Button
v-if="recipes.length && !showSearch && !selectMode" :hidden="showSearch || selectMode"
class="ico"
:text="icon.sort"
col="3"
@tap="openSort"
/>
<Button
v-if="recipes.length && !showSearch && !selectMode"
class="ico"
:text="icon.sort"
col="4"
@tap="openFilters"
/>
<Button
v-if="!showSearch && !selectMode"
class="ico fab" class="ico fab"
:text="icon.plus" :text="icon.plus"
col="5" col="5"
@tap="addRecipe()" @tap="addRecipe"
/> />
<Button <Button
v-if="selectMode" :hidden="!selectMode"
class="ico" class="ico"
:text="icon.del" :text="icon.del"
col="5" col="5"
@ -375,6 +407,7 @@ import {
Device, Device,
Screen, Screen,
Color, Color,
CoreTypes,
} from "@nativescript/core"; } from "@nativescript/core";
import { localize } from "@nativescript/localize"; import { localize } from "@nativescript/localize";
import { import {
@ -390,7 +423,7 @@ import Settings from "./Settings";
import ActionDialog from "./modal/ActionDialog.vue"; import ActionDialog from "./modal/ActionDialog.vue";
import ConfirmDialog from "./modal/ConfirmDialog.vue"; import ConfirmDialog from "./modal/ConfirmDialog.vue";
import Filters from "./modal/Filters.vue"; import Filters from "./modal/Filters.vue";
import * as utils from "~/shared/utils.js"; import * as utils from "~/shared/utils";
let lastTime = 0; let lastTime = 0;
let lastShake = 0; let lastShake = 0;
let lastForce = 0; let lastForce = 0;
@ -416,17 +449,14 @@ export default {
topmenu: [ topmenu: [
{ {
title: "EnRecipes", title: "EnRecipes",
component: "EnRecipes",
icon: "home", icon: "home",
}, },
{ {
title: "trylater", title: "trylater",
component: "Try Later",
icon: "try", icon: "try",
}, },
{ {
title: "favourites", title: "favourites",
component: "Favourites",
icon: "fav", icon: "fav",
}, },
], ],
@ -519,13 +549,13 @@ export default {
methods: { methods: {
...mapActions([ ...mapActions([
"setComponent", "setComponent",
"setSortTypeAction",
"initListItems", "initListItems",
"initRecipes", "initRecipes",
"initMealPlans", "initMealPlans",
"setShakeAction", "setShake",
"setFirstDay",
"setLayout", "setLayout",
"setSortTypeAction", "setSortType",
"deleteRecipeAction", "deleteRecipeAction",
"deleteRecipesAction", "deleteRecipesAction",
"clearFilter", "clearFilter",
@ -536,6 +566,9 @@ export default {
const window = Application.android.startActivity.getWindow(); const window = Application.android.startActivity.getWindow();
const decorView = window.getDecorView(); const decorView = window.getDecorView();
let sdkv = Device.sdkVersion; let sdkv = Device.sdkVersion;
this.appTheme == "Light"
? decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
: decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_DARK_STATUS_BAR);
function setColors(color) { function setColors(color) {
window.setStatusBarColor(new Color(color).android); window.setStatusBarColor(new Color(color).android);
sdkv >= 27 && window.setNavigationBarColor(new Color(color).android); sdkv >= 27 && window.setNavigationBarColor(new Color(color).android);
@ -551,32 +584,29 @@ export default {
setColors("#000000"); setColors("#000000");
break; break;
} }
sdkv >= 23 && this.appTheme == "Light" if (sdkv >= 27)
? decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) this.appTheme == "Light"
: decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_DARK_STATUS_BAR); ? decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
sdkv >= 27 && this.appTheme == "Light" )
? decorView.setSystemUiVisibility( : decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR View.SYSTEM_UI_FLAG_DARK_NAVIGATION_BAR
) );
: decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_DARK_NAVIGATION_BAR
);
}, },
onPageLoad(args) { onPageLoad(args) {
const page = args.object; const page = args.object;
page.bindingContext = new Observable(); page.bindingContext = new Observable();
this.filterFavourites this.filterFavourites
? this.setComponent("Favourites") ? this.setComponent("favourites")
: this.filterTrylater : this.filterTrylater
? this.setComponent("Try Later") ? this.setComponent("trylater")
: this.selectedCuisine : this.selectedCuisine
? this.setComponent("Filtered recipes") ? this.setComponent("Filtered recipes")
: this.setComponent("EnRecipes"); : this.setComponent("EnRecipes");
if (this.shakeEnabled) { if (this.shakeEnabled) {
if (utils.hasAccelerometer()) if (utils.hasAccelerometer())
startAccelerometerUpdates((data) => this.onSensorData(data)); startAccelerometerUpdates((data) => this.onSensorData(data));
else this.setShakeAction(false); else this.setShake(false);
} }
this.hijackBackEvent(); this.hijackBackEvent();
setTimeout(() => { setTimeout(() => {
@ -606,22 +636,25 @@ export default {
this.scrollPos = Math.abs(y); this.scrollPos = Math.abs(y);
let ab = this.appbar.translateY; let ab = this.appbar.translateY;
if (!scrollUp && ab == 0) { if (!scrollUp && ab == 0) {
this.animateInOut( this.appbar.animate({
250, translate: { x: 0, y: 64 },
false, duration: 250,
(val) => (this.appbar.translateY = val * 64) curve: CoreTypes.AnimationCurve.ease,
); });
} else if (scrollUp && ab == 64) { } else if (scrollUp && ab == 64) {
this.animateInOut( this.appbar.animate({
250, translate: { x: 0, y: 0 },
true, duration: 250,
(val) => (this.appbar.translateY = val * 64) curve: CoreTypes.AnimationCurve.ease,
); });
} }
} }
}, },
getSpanSize(index) { getSpanSize(index) {
return this.layout == "grid" && (index == 0 || index == 1) ? 2 : 1; return (this.layout == "grid" || this.layout == "photogrid") &&
(index == 0 || index == 1)
? 2
: 1;
}, },
getLayout(args, index, items) { getLayout(args, index, items) {
return index == 0 ? "header" : index == 1 ? "lists" : this.layout; return index == 0 ? "header" : index == 1 ? "lists" : this.layout;
@ -646,7 +679,7 @@ export default {
props: { props: {
title: "srt", title: "srt",
list: [ list: [
"Title", "title",
"Rating", "Rating",
"Quickest first", "Quickest first",
"Slowest first", "Slowest first",
@ -658,7 +691,7 @@ export default {
}, },
}).then((action) => { }).then((action) => {
if (action && action !== "Cancel" && this.sortType !== action) { if (action && action !== "Cancel" && this.sortType !== action) {
this.setSortTypeAction(action); this.setSortType(action);
ApplicationSettings.setString("sortType", action); ApplicationSettings.setString("sortType", action);
this.updateSort(); this.updateSort();
} }
@ -668,6 +701,8 @@ export default {
//FILTER //FILTER
openFilters() { openFilters() {
this.setComponent("EnRecipes");
this.filterFavourites = this.filterTrylater = false;
this.showTools = false; this.showTools = false;
this.releaseBackEvent(); this.releaseBackEvent();
this.$showModal(Filters).then(() => this.hijackBackEvent()); this.$showModal(Filters).then(() => this.hijackBackEvent());
@ -917,35 +952,27 @@ export default {
let dl1 = difficultyLevel(a.difficulty); let dl1 = difficultyLevel(a.difficulty);
let dl2 = difficultyLevel(b.difficulty); let dl2 = difficultyLevel(b.difficulty);
switch (this.sortType) { switch (this.sortType) {
case "Title": case "title":
return titleOrder > 0 ? 1 : titleOrder < 0 ? -1 : 0; return titleOrder > 0 ? 1 : titleOrder < 0 ? -1 : 0;
break;
case "Quickest first": case "Quickest first":
return d1 > d2 ? 1 : d1 < d2 ? -1 : 0; return d1 > d2 ? 1 : d1 < d2 ? -1 : 0;
break;
case "Slowest first": case "Slowest first":
return d1 > d2 ? -1 : d1 < d2 ? 1 : 0; return d1 > d2 ? -1 : d1 < d2 ? 1 : 0;
break;
case "Rating": case "Rating":
return r1 > r2 ? -1 : r1 < r2 ? 1 : 0; return r1 > r2 ? -1 : r1 < r2 ? 1 : 0;
break;
case "Difficulty level": case "Difficulty level":
return dl1 > dl2 ? 1 : dl1 < dl2 ? -1 : 0; return dl1 > dl2 ? 1 : dl1 < dl2 ? -1 : 0;
break;
case "Last updated": case "Last updated":
return ld1 < ld2 ? 1 : ld1 > ld2 ? -1 : 0; return ld1 < ld2 ? 1 : ld1 > ld2 ? -1 : 0;
break;
case "Newest first": case "Newest first":
return cd1 < cd2 ? 1 : cd1 > cd2 ? -1 : 0; return cd1 < cd2 ? 1 : cd1 > cd2 ? -1 : 0;
break;
case "Oldest first": case "Oldest first":
return cd1 < cd2 ? -1 : cd1 > cd2 ? 1 : 0; return cd1 < cd2 ? -1 : cd1 > cd2 ? 1 : 0;
break;
} }
}, },
getItemPos(id) { getItemPos(id) {
let length = this.filteredRecipes.length; let length = this.filteredRecipes.length;
let l2 = this.layout == "grid"; let l2 = this.layout == "grid" || this.layout == "photogrid";
let oddOrEven = this.oddOrEven(id); let oddOrEven = this.oddOrEven(id);
let itemPos = let itemPos =
id == this.filteredRecipes[0].id || id == this.filteredRecipes[0].id ||
@ -995,7 +1022,7 @@ export default {
args.cancel = true; args.cancel = true;
this.clearSelection(); this.clearSelection();
} else if ( } else if (
["Favourites", "Try Later", "Filtered recipes"].includes( ["favourites", "trylater", "Filtered recipes"].includes(
this.currentComponent this.currentComponent
) )
) { ) {
@ -1019,9 +1046,9 @@ export default {
}, },
}); });
} else if (title !== this.currentComponent) { } else if (title !== this.currentComponent) {
this.setComponent(to); this.setComponent(title);
this.filterFavourites = to == "Favourites"; this.filterFavourites = to == "favourites";
this.filterTrylater = to == "Try Later"; this.filterTrylater = to == "trylater";
this.clearFilter(); this.clearFilter();
} }
}, },
@ -1051,11 +1078,9 @@ export default {
duration: 250, duration: 250,
curve: "easeOut", curve: "easeOut",
}, },
// backstackVisible: false,
}); });
}, },
viewRandomRecipe() { viewRandomRecipe() {
// this.showFAB =
this.showTools = false; this.showTools = false;
this.$navigateTo(ViewRecipe, { this.$navigateTo(ViewRecipe, {
props: { props: {
@ -1067,15 +1092,14 @@ export default {
duration: 250, duration: 250,
curve: "easeOut", curve: "easeOut",
}, },
backstackVisible: false,
}); });
}, },
touchSelector({ object, action }, comp) { touchSelector({ object, action }, comp, title) {
let selected = this.currentComponent == comp; let selected = this.currentComponent == comp;
object.className = action.match(/down|move/) object.className = action.match(/down|move/)
? `segment ${selected ? "select" : "fade"}` ? `segment ${selected ? "select" : "fade"}`
: `segment ${selected && "select"}`; : `segment ${selected && "select"}`;
if (action == "up") this.navigateTo(comp, comp); if (action == "up") this.navigateTo(comp, title);
}, },
touchTool({ object, action }, comp, value) { touchTool({ object, action }, comp, value) {
object.className = action.match(/down|move/) ? `tool fade` : `tool`; object.className = action.match(/down|move/) ? `tool fade` : `tool`;
@ -1088,7 +1112,8 @@ export default {
if (!this.recipes.length) this.initRecipes(); if (!this.recipes.length) this.initRecipes();
this.initListItems(); this.initListItems();
if (!this.mealPlans.length) this.initMealPlans(); if (!this.mealPlans.length) this.initMealPlans();
this.setShakeAction(ApplicationSettings.getBoolean("shakeEnabled", true)); this.setShake(ApplicationSettings.getBoolean("shakeEnabled", true));
this.setFirstDay(ApplicationSettings.getBoolean("mondayFirst", false));
}, },
}; };
</script> </script>

View file

@ -30,7 +30,7 @@
class="dayName" class="dayName"
row="1" row="1"
:col="i" :col="i"
v-for="(d, i) in dNames" v-for="(d, i) in getDayNames"
:key="d" :key="d"
:text="d | L" :text="d | L"
/> />
@ -94,14 +94,14 @@
row="1" row="1"
@loaded="onAppBarLoad" @loaded="onAppBarLoad"
class="appbar" class="appbar"
v-show="!showUndo" :hidden="showUndo"
columns="auto, *, auto, auto" columns="auto, *, auto, auto"
> >
<Button class="ico" :text="icon.back" @tap="$navigateBack()" /> <Button class="ico" :text="icon.back" @tap="$navigateBack()" />
<Button <Button
class="ico" class="ico"
:text="icon.tod" :text="icon.tod"
v-if="!isExactlyToday" :hidden="isExactlyToday"
@tap="goToToday" @tap="goToToday"
col="2" col="2"
/> />
@ -115,19 +115,19 @@
<GridLayout <GridLayout
row="1" row="1"
class="appbar snackBar" class="appbar snackBar"
v-show="showUndo" :hidden="!showUndo"
columns="auto, *, auto" columns="auto, *, auto"
> >
<Button :text="countdown" class="countdown tb" /> <Button :text="countdown" class="ico countdown tb" />
<Label class="title" col="1" :text="snackMsg | L" /> <Label class="title" col="1" :text="snackMsg | L" />
<Button class="ico fab" :text="icon.alert" @tap="undoDel" col="3" /> <Button class="ico fab" :text="icon.undo" @tap="undoDel" col="3" />
</GridLayout> </GridLayout>
</GridLayout> </GridLayout>
</Page> </Page>
</template> </template>
<script> <script>
import { ApplicationSettings, Observable } from "@nativescript/core"; import { ApplicationSettings, Observable, CoreTypes } from "@nativescript/core";
import { mapState, mapActions } from "vuex"; import { mapState, mapActions } from "vuex";
import ViewRecipe from "./ViewRecipe.vue"; import ViewRecipe from "./ViewRecipe.vue";
import ActionDialogWithSearch from "./modal/ActionDialogWithSearch.vue"; import ActionDialogWithSearch from "./modal/ActionDialogWithSearch.vue";
@ -138,7 +138,7 @@ export default {
return { return {
appTheme: "Light", appTheme: "Light",
mealTimes: ["breakfast", "lunch", "dinner", "snacks"], mealTimes: ["breakfast", "lunch", "dinner", "snacks"],
dNames: ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"], dNames: ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"],
year: 2021, year: 2021,
mNames: [ mNames: [
"January", "January",
@ -166,7 +166,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(["icon", "recipes", "mealPlans"]), ...mapState(["icon", "recipes", "mealPlans", "mondayFirst"]),
todaysTime() { todaysTime() {
return new Date(this.year, this.month, this.today, 0).getTime(); return new Date(this.year, this.month, this.today, 0).getTime();
}, },
@ -180,13 +180,16 @@ export default {
}, {}); }, {});
} else return 0; } else return 0;
}, },
getDayNames() {
if (!this.mondayFirst) this.dNames.unshift(this.dNames.pop());
return this.dNames;
},
getCal() { getCal() {
let y = this.year; let y = this.year;
let m = this.month; let m = this.month;
let t = this.today;
let d = new Date(y, m, t);
let ds = new Date(y, m + 1, 0).getDate(); let ds = new Date(y, m + 1, 0).getDate();
let fd = new Date(y, m, 1).getDay(); let fd = new Date(y, m, 1).getDay();
if (this.mondayFirst) fd -= 1;
let days = new Array(fd).fill(0); let days = new Array(fd).fill(0);
for (let i = 1; i <= ds; i++) { for (let i = 1; i <= ds; i++) {
days.push(i); days.push(i);
@ -230,17 +233,17 @@ export default {
this.scrollPos = Math.abs(y); this.scrollPos = Math.abs(y);
let ab = this.appbar.translateY; let ab = this.appbar.translateY;
if (!scrollUp && ab == 0) { if (!scrollUp && ab == 0) {
this.animateInOut( this.appbar.animate({
250, translate: { x: 0, y: 64 },
false, duration: 250,
(val) => (this.appbar.translateY = val * 64) curve: CoreTypes.AnimationCurve.ease,
); });
} else if (scrollUp && ab == 64) { } else if (scrollUp && ab == 64) {
this.animateInOut( this.appbar.animate({
250, translate: { x: 0, y: 0 },
true, duration: 250,
(val) => (this.appbar.translateY = val * 64) curve: CoreTypes.AnimationCurve.ease,
); });
} }
} }
}, },
@ -275,7 +278,6 @@ export default {
duration: 250, duration: 250,
curve: "easeOut", curve: "easeOut",
}, },
// backstackVisible: false,
}); });
} }
}, },
@ -362,11 +364,11 @@ export default {
}, },
showUndoBar(message) { showUndoBar(message) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
clearTimeout(undoTimer);
this.showUndo = true; this.showUndo = true;
this.snackMsg = message; this.snackMsg = message;
this.countdown = 5; this.countdown = 5;
let a = 5; let a = 5;
clearTimeout(undoTimer);
undoTimer = setInterval(() => { undoTimer = setInterval(() => {
if (this.undo) { if (this.undo) {
this.showUndo = this.undo = false; this.showUndo = this.undo = false;

View file

@ -5,13 +5,12 @@
colSpan="2" colSpan="2"
rowSpan="2" rowSpan="2"
class="options-list" class="options-list"
@loaded="listViewLoad"
for="item in items" for="item in items"
> >
<v-template if="$index == 0"> <v-template if="$index == 0">
<Label class="pageTitle" :text="'Settings' | L" /> <Label class="pageTitle" :text="'Settings' | L" />
</v-template> </v-template>
<v-template if="$index == 7"> <v-template if="$index == 6">
<StackLayout class="listSpace"> </StackLayout> <StackLayout class="listSpace"> </StackLayout>
</v-template> </v-template>
<v-template> <v-template>
@ -30,11 +29,7 @@
</v-template> </v-template>
</ListView> </ListView>
<GridLayout row="1" class="appbar" rows="*" columns="auto, *"> <GridLayout row="1" class="appbar" rows="*" columns="auto, *">
<Button <Button class="ico" :text="icon.back" @tap="$navigateBack()" />
class="ico"
:text="icon.back"
@tap="$navigateBack()"
/>
</GridLayout> </GridLayout>
</GridLayout> </GridLayout>
</Page> </Page>
@ -47,7 +42,6 @@ import Interface from "./Settings/Interface.vue";
import Options from "./Settings/Options.vue"; import Options from "./Settings/Options.vue";
import Database from "./Settings/Database.vue"; import Database from "./Settings/Database.vue";
import Reset from "./Settings/Reset.vue"; import Reset from "./Settings/Reset.vue";
import Help from "./Settings/Help.vue";
import About from "./Settings/About.vue"; import About from "./Settings/About.vue";
export default { export default {
data() { data() {
@ -55,17 +49,17 @@ export default {
items: [ items: [
{}, {},
{ {
icon: "theme", icon: "interface",
title: "intf", title: "intf",
view: Interface, view: Interface,
}, },
{ {
icon: "cog", icon: "opts",
title: "opts", title: "opts",
view: Options, view: Options,
}, },
{ {
icon: "exp", icon: "db",
title: "db", title: "db",
view: Database, view: Database,
}, },
@ -74,11 +68,6 @@ export default {
title: "rest", title: "rest",
view: Reset, view: Reset,
}, },
{
icon: "help",
title: "help",
view: Help,
},
{ {
icon: "info", icon: "info",
title: "About", title: "About",

View file

@ -6,7 +6,6 @@
colSpan="2" colSpan="2"
class="options-list" class="options-list"
for="item in items" for="item in items"
@loaded="listViewLoad"
> >
<v-template if="$index == 0"> <v-template if="$index == 0">
<Label class="pageTitle" :text="'About' | L" /> <Label class="pageTitle" :text="'About' | L" />
@ -24,7 +23,7 @@
<Label class="info tac tw" :text="'appInfo' | L" /> <Label class="info tac tw" :text="'appInfo' | L" />
</StackLayout> </StackLayout>
</v-template> </v-template>
<v-template if="$index == 6"> <v-template if="$index == 8">
<StackLayout class="listSpace"> </StackLayout> <StackLayout class="listSpace"> </StackLayout>
</v-template> </v-template>
<v-template> <v-template>
@ -39,11 +38,7 @@
</v-template> </v-template>
</ListView> </ListView>
<GridLayout row="1" class="appbar" rows="*" columns="auto, *"> <GridLayout row="1" class="appbar" rows="*" columns="auto, *">
<Button <Button class="ico" :text="icon.back" @tap="$navigateBack()" />
class="ico"
:text="icon.back"
@tap="$navigateBack()"
/>
</GridLayout> </GridLayout>
</GridLayout> </GridLayout>
</Page> </Page>
@ -65,6 +60,16 @@ export default {
title: "gh", title: "gh",
url: "https://github.com/vishnuraghavb/EnRecipes", url: "https://github.com/vishnuraghavb/EnRecipes",
}, },
{
icon: "tg",
title: "joinTG",
url: "https://t.me/enrecipes",
},
{
icon: "help",
title: "guide",
url: "https://github.com/vishnuraghavb/EnRecipes/wiki/User-Guide",
},
{ {
icon: "priv", icon: "priv",
title: "priv", title: "priv",

View file

@ -1,15 +0,0 @@
<template>
<Page>
<GridLayout class="progressContainer" columns="*, 64">
<Progress col="0" :value="backupProgress" maxValue="100" />
<Label col="1" :text="` ${backupProgress}%`" />
</GridLayout>
</Page>
</template>
<script>
export default {};
</script>
<style>
</style>

View file

@ -28,7 +28,7 @@
</v-template> </v-template>
</ListView> </ListView>
<GridLayout <GridLayout
v-show="!toast && !backupProgress" v-show="!toast && !progress"
row="1" row="1"
class="appbar" class="appbar"
rows="*" rows="*"
@ -49,13 +49,14 @@
</FlexboxLayout> </FlexboxLayout>
</GridLayout> </GridLayout>
<GridLayout <GridLayout
v-show="backupProgress" v-show="progress"
row="1" row="1"
colSpan="2" colSpan="2"
class="appbar snackBar" class="appbar snackBar"
columns="*" columns="auto, *"
> >
<Progress :value="backupProgress" maxValue="100" /> <ActivityIndicator :busy="progress ? true : false" />
<Label col="1" class="title" :text="progress" textWrap="true" />
</GridLayout> </GridLayout>
</GridLayout> </GridLayout>
</Page> </Page>
@ -71,9 +72,6 @@ import {
Observable, Observable,
Application, Application,
} from "@nativescript/core"; } from "@nativescript/core";
import * as Permissions from "@nativescript-community/perms";
import { Zip } from "@nativescript/zip";
import { openFilePicker } from "@nativescript-community/ui-document-picker";
import { localize } from "@nativescript/localize"; import { localize } from "@nativescript/localize";
import ConfirmDialog from "../modal/ConfirmDialog.vue"; import ConfirmDialog from "../modal/ConfirmDialog.vue";
import { mapState, mapActions } from "vuex"; import { mapState, mapActions } from "vuex";
@ -84,7 +82,7 @@ export default {
data() { data() {
return { return {
backupFolder: null, backupFolder: null,
backupProgress: 0, progress: null,
toast: null, toast: null,
}; };
}, },
@ -118,7 +116,6 @@ export default {
icon: "imp", icon: "imp",
title: "impBu", title: "impBu",
subTitle: localize("impInfo"), subTitle: localize("impInfo"),
// action: this.importCheck,
action: this.openZipFile, action: this.openZipFile,
}, },
{}, {},
@ -131,43 +128,63 @@ export default {
"importRecipesAction", "importRecipesAction",
"importMealPlansAction", "importMealPlansAction",
"unlinkBrokenImages", "unlinkBrokenImages",
"clearImportSummary",
]), ]),
onPageLoad(args) { onPageLoad(args) {
const page = args.object; const page = args.object;
page.bindingContext = new Observable(); page.bindingContext = new Observable();
const downloadsFolder = Folder.fromPath( const ContentResolver = Application.android.nativeApp.getContentResolver();
android.os.Environment.getExternalStorageDirectory().getAbsolutePath() this.backupFolder = ApplicationSettings.getString("backupFolder");
).getFolder("Download").path; if (
this.backupFolder = ApplicationSettings.getString( !this.backupFolder ||
"backupFolder", ContentResolver.getPersistedUriPermissions().isEmpty()
downloadsFolder ) {
); this.backupFolder = null;
}
}, },
// BACKUP FOLDER PICKER // BACKUP FOLDER PICKER
setBackupFolder() { setBackupFolder(startExport) {
utils.pickFolder().then((res) => { const ContentResolver = Application.android.nativeApp.getContentResolver();
if (res != null) { const FLAGS =
this.backupFolder = res; android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION |
android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
utils.getBackupFolder().then((uri) => {
if (uri != null) {
if (this.backupFolder)
ContentResolver.releasePersistableUriPermission(
new android.net.Uri.parse(this.backupFolder),
FLAGS
);
this.backupFolder = uri.toString();
ApplicationSettings.setString("backupFolder", this.backupFolder); ApplicationSettings.setString("backupFolder", this.backupFolder);
// PERSIST PERMISSIONS
ContentResolver.takePersistableUriPermission(uri, FLAGS);
if (startExport && this.backupFolder) {
this.exportBackup();
}
} }
}); });
}, },
// EXPORT HANDLERS // EXPORT HANDLERS
exportCheck() { exportCheck() {
const ContentResolver = Application.android.nativeApp.getContentResolver();
if (!this.recipes.length) { if (!this.recipes.length) {
this.toast = localize("aFBu"); this.toast = localize("aFBu");
utils.timer(5, (val) => { utils.timer(5, (val) => {
if (!val) this.toast = val; if (!val) this.toast = val;
}); });
} else { } else {
this.permissionCheck( if (
this.permissionConfirmation, !this.backupFolder ||
localize("reqAcc"), ContentResolver.getPersistedUriPermissions().isEmpty()
this.exportBackup ) {
); this.setBackupFolder(true);
} else this.exportBackup();
} }
}, },
exportBackup() { exportBackup() {
this.progress = localize("expip");
this.exportFiles("create"); this.exportFiles("create");
let date = new Date(); let date = new Date();
let formattedDate = let formattedDate =
@ -183,17 +200,11 @@ export default {
let filename = `EnRecipes_${formattedDate}.zip`; let filename = `EnRecipes_${formattedDate}.zip`;
let fromPath = path.join(knownFolders.documents().path, "EnRecipes"); let fromPath = path.join(knownFolders.documents().path, "EnRecipes");
let sdcard = android.os.Environment.isExternalStorageManager(); utils.Zip.zip(fromPath, this.backupFolder, filename)
let destPath = path.join(this.backupFolder, filename); .then((res) => {
console.log(sdcard); if (res) this.showExportSummary(filename);
Zip.zip({ })
directory: fromPath, .catch((err) => console.log("Backup error: ", err));
archive: destPath,
onProgress: (progress) => (this.backupProgress = progress),
}).then(() => {
this.showExportSummary(filename);
this.exportFiles("delete");
});
}, },
exportFiles(option) { exportFiles(option) {
const folder = path.join(knownFolders.documents().path, "EnRecipes"); const folder = path.join(knownFolders.documents().path, "EnRecipes");
@ -240,80 +251,143 @@ export default {
this.units.length && userUnitsFile.remove(); this.units.length && userUnitsFile.remove();
this.mealPlans.length && mealPlansFile.remove(); this.mealPlans.length && mealPlansFile.remove();
break; break;
default:
break;
} }
}, },
writeDataToFile(file, data) { writeDataToFile(file, data) {
file.writeText(JSON.stringify(data)); file.writeText(JSON.stringify(data));
}, },
// IMPORT HANDLERS showExportSummary(filename) {
importCheck() { this.progress = null;
this.permissionCheck( this.$showModal(ConfirmDialog, {
this.permissionConfirmation, props: {
localize("reqAcc"), title: "expSuc",
this.openZipFile description: `Backed up to ${filename}`,
); okButtonText: "OK",
},
});
}, },
openZipFile() {
const ContentResolver = Application.android.nativeApp.getContentResolver();
// IMPORT HANDLERS
openZipFile() {
utils.getBackupFile().then((uri) => { utils.getBackupFile().then((uri) => {
console.log(uri); console.log(uri);
const inputStream = ContentResolver.openInputStream(uri); if (uri) {
console.log(inputStream); let dest = path.join(knownFolders.temp().path, "tempUnZip");
let newFile = knownFolders.temp().getFile("test.zip"); utils.Zip.unzip(uri, dest)
console.log(newFile); .then((res) => {
try { if (res) this.validateZipContent(res, uri);
const input = new java.io.BufferedInputStream(inputStream); })
console.log(input); .catch(() => this.failedImport(localize("buInc")));
const outputStream = new java.io.BufferedOutputStream(
new java.io.FileOutputStream(newFile.path)
);
console.log(outputStream);
let size = inputStream.available();
console.log(size);
let buffer = Array.create("byte", size);
input.read(buffer);
do {
outputStream.write(buffer);
} while (input.read(buffer) != -1);
} catch (e) {
console.log(e);
} finally {
outputStream.flush();
outputStream.close();
input.close();
} }
// console.log(zipInputStream);
// console.log(zipInputStream);
// let extractedFile = new java.io.File(
// knownFolders.temp().path + "/tmp.zip"
// );
// if (!extractedFile.exists()) {
// extractedFile.mkdirs();
// }
// console.log(extractedFile);
// let outputStream = new java.io.OutputStream(extractedFile);
// android.os.FileUtils.copy(inputStream, outputStream);
// let outputStream = new java.io.FileOutputStream(extractedFile);
// console.log(outputStream);
// while ((readLen = zipInputStream.read(readBuffer)) != -1) {
// outputStream.write(readBuffer, 0, readLen);
// }
// console.log(outputStream, 2);
}); });
// openFilePicker({
// extensions: ["zip"],
// }).then((res) => this.validateZipContent(res.files[0]));
}, },
importDataToDB(data, db, zipPath) { validateZipContent(extractedFolderPath, uri) {
this.progress = localize("impip");
let cacheFolderPath = extractedFolderPath + "/EnRecipes";
const EnRecipesFilePath = cacheFolderPath + "/recipes.json";
const ImagesFolderPath = cacheFolderPath + "/Images";
const userCuisinesFilePath = cacheFolderPath + "/userCuisines.json";
const userCategoriesFilePath = cacheFolderPath + "/userCategories.json";
const userYieldUnitsFilePath = cacheFolderPath + "/userYieldUnits.json";
const userUnitsFilePath = cacheFolderPath + "/userUnits.json";
const mealPlansFilePath = cacheFolderPath + "/mealPlans.json";
if (Folder.exists(cacheFolderPath)) {
this.isFileDataValid([
{
path: EnRecipesFilePath,
db: "EnRecipesDB",
file: "recipes.json",
},
{
path: userCuisinesFilePath,
db: "userCuisinesDB",
file: "userCuisines.json",
},
{
path: userCategoriesFilePath,
db: "userCategoriesDB",
file: "userCategories.json",
},
{
path: userYieldUnitsFilePath,
db: "userYieldUnitsDB",
file: "userYieldUnits.json",
},
{
path: userUnitsFilePath,
db: "userUnitsDB",
file: "userUnits.json",
},
{
path: mealPlansFilePath,
db: "mealPlansDB",
file: "mealPlans.json",
},
]);
} else {
Folder.fromPath(extractedFolderPath).remove();
this.progress = null;
this.failedImport(localize("buInc"));
}
if (Folder.exists(ImagesFolderPath)) {
const timer = setInterval(() => {
if (this.importSummary.found) {
this.importImages(uri, extractedFolderPath);
clearInterval(timer);
}
}, 100);
}
},
isFileDataValid(file) {
const files = file.filter((e) => File.exists(e.path));
if (files.length) {
let isValid = files.map((e) => false);
files.forEach((file, i) => {
File.fromPath(file.path)
.readText()
.then((data) => {
isValid[i] = this.hasValidJSON(data);
if (!isValid[i]) {
this.failedImport(
`${localize("buMod")}\n\n${localize("invFile")}: ${file.file}`
);
return 0;
}
if (isValid.every((e) => e === true)) {
files.forEach((file) => {
File.fromPath(file.path)
.readText()
.then((data) => {
this.importDataToDB(JSON.parse(data), file.db);
});
});
}
});
});
} else {
this.failedImport(localize("buEmp"));
}
},
failedImport(description) {
this.$showModal(ConfirmDialog, {
props: {
title: "impFail",
description,
okButtonText: "OK",
},
});
},
hasValidJSON(data) {
try {
JSON.parse(data) && Array.isArray(JSON.parse(data));
} catch (e) {
return false;
}
return true;
},
importDataToDB(data, db) {
switch (db) { switch (db) {
case "EnRecipesDB": case "EnRecipesDB":
this.importImages(zipPath);
this.importRecipesAction(data); this.importRecipesAction(data);
break; break;
case "userCuisinesDB": case "userCuisinesDB":
@ -343,133 +417,18 @@ export default {
case "mealPlansDB": case "mealPlansDB":
this.importMealPlansAction(data); this.importMealPlansAction(data);
break; break;
default:
break;
} }
}, },
hasValidJSON(data) { importImages(uri, extractedFolderPath) {
try { let destPath = knownFolders.documents().path;
JSON.parse(data) && Array.isArray(JSON.parse(data)); Folder.fromPath(destPath);
} catch (e) { utils.Zip.unzip(uri, destPath).then((res) => {
return false; if (res) {
} this.showImportSummary();
return true; this.unlinkBrokenImages();
}, this.exportFiles("delete");
isFileDataValid(file) {
const files = file.filter((e) => File.exists(e.path));
if (files.length) {
let isValid = files.map((e) => false);
files.forEach((file, i) => {
File.fromPath(file.path)
.readText()
.then((data) => {
isValid[i] = this.hasValidJSON(data);
if (!isValid[i]) {
this.failedImport(
`${localize("buMod")}\n\n${localize("invFile")}: ${file.file}`
);
return 0;
}
if (isValid.every((e) => e === true)) {
files.forEach((file, i) => {
File.fromPath(file.path)
.readText()
.then((data) => {
this.importDataToDB(
JSON.parse(data),
file.db,
file.zipPath
);
});
});
}
});
});
} else {
this.failedImport(localize("buEmp"));
}
},
failedImport(description) {
this.$showModal(ConfirmDialog, {
props: {
title: "impFail",
description,
okButtonText: "OK",
},
});
},
validateZipContent(zipPath) {
console.log(zipPath);
Zip.unzip({
archive: zipPath,
overwrite: true,
onProgress: (progress) => (this.backupProgress = progress),
}).then((extractedFolderPath) => {
let cacheFolderPath = extractedFolderPath + "/EnRecipes";
const EnRecipesFilePath = cacheFolderPath + "/recipes.json";
const userCuisinesFilePath = cacheFolderPath + "/userCuisines.json";
const userCategoriesFilePath = cacheFolderPath + "/userCategories.json";
const userYieldUnitsFilePath = cacheFolderPath + "/userYieldUnits.json";
const userUnitsFilePath = cacheFolderPath + "/userUnits.json";
const mealPlansFilePath = cacheFolderPath + "/mealPlans.json";
if (Folder.exists(cacheFolderPath)) {
this.isFileDataValid([
{
zipPath,
path: EnRecipesFilePath,
db: "EnRecipesDB",
file: "recipes.json",
},
{
zipPath,
path: userCuisinesFilePath,
db: "userCuisinesDB",
file: "userCuisines.json",
},
{
zipPath,
path: userCategoriesFilePath,
db: "userCategoriesDB",
file: "userCategories.json",
},
{
zipPath,
path: userYieldUnitsFilePath,
db: "userYieldUnitsDB",
file: "userYieldUnits.json",
},
{
zipPath,
path: userUnitsFilePath,
db: "userUnitsDB",
file: "userUnits.json",
},
{
zipPath,
path: mealPlansFilePath,
db: "mealPlansDB",
file: "mealPlans.json",
},
]);
} else {
Folder.fromPath(extractedFolderPath).remove(); Folder.fromPath(extractedFolderPath).remove();
this.failedImport(localize("buInc"));
} }
if (Folder.exists(cacheFolderPath + "/Images")) {
this.importImages(cacheFolderPath + "/Images");
}
});
},
importImages(sourcePath) {
let dest = knownFolders.documents().path;
Zip.unzip({
archive: sourcePath,
directory: dest,
overwrite: true,
onProgress: (progress) => (this.backupProgress = progress),
}).then((res) => {
this.showImportSummary();
this.unlinkBrokenImages();
}); });
}, },
showImportSummary() { showImportSummary() {
@ -478,6 +437,7 @@ export default {
let importedNote = `\n${imported} ${localize("recI")}`; let importedNote = `\n${imported} ${localize("recI")}`;
let existsNote = `\n${exists} ${localize("recE")}`; let existsNote = `\n${exists} ${localize("recE")}`;
let updatedNote = `\n${updated} ${localize("recU")}`; let updatedNote = `\n${updated} ${localize("recU")}`;
this.progress = null;
this.$showModal(ConfirmDialog, { this.$showModal(ConfirmDialog, {
props: { props: {
title: "impSuc", title: "impSuc",
@ -486,51 +446,9 @@ export default {
)}${importedNote}${existsNote}${updatedNote}`, )}${importedNote}${existsNote}${updatedNote}`,
okButtonText: "OK", okButtonText: "OK",
}, },
}).then(() => (this.backupProgress = 0)); }).then(() => this.clearImportSummary());
},
showExportSummary(filename) {
this.$showModal(ConfirmDialog, {
props: {
title: "expSuc",
description: `Backed up to ${filename}`,
okButtonText: "OK",
},
}).then(() => (this.backupProgress = 0));
},
// PERMISSIONS HANDLER
permissionCheck(confirmation, description, action) {
if (!ApplicationSettings.getBoolean("storagePermissionAsked", false)) {
confirmation(description).then((e) => {
if (e) {
Permissions.request("photo").then((res) => {
let status = res[Object.keys(res)[0]];
if (status === "authorized") action();
if (status !== "denied")
ApplicationSettings.setBoolean("storagePermissionAsked", true);
});
}
});
} else {
Permissions.check("photo").then((res) => {
let status = res[Object.keys(res)[0]];
if (status !== "authorized") {
confirmation(description).then((e) => {
e && utils.openAppSettingsPage();
});
} else action();
});
}
},
permissionConfirmation(description) {
return this.$showModal(ConfirmDialog, {
props: {
title: "grant",
description,
cancelButtonText: "nNBtn",
okButtonText: "conBtn",
},
});
}, },
// HELPERS // HELPERS
touch({ object, action }, method) { touch({ object, action }, method) {
object.className = action.match(/down|move/) ? "option fade" : "option"; object.className = action.match(/down|move/) ? "option fade" : "option";

View file

@ -1,78 +0,0 @@
<template>
<Page @loaded="onPageLoad" actionBarHidden="true">
<GridLayout rows="*, auto" columns="auto, *">
<ListView
colSpan="2"
rowSpan="2"
class="options-list"
for="item in items"
@loaded="listViewLoad"
>
<v-template if="$index == 0">
<Label class="pageTitle" :text="'help' | L" />
</v-template>
<v-template if="$index == 3">
<StackLayout class="listSpace"> </StackLayout>
</v-template>
<v-template>
<GridLayout
columns="auto, *"
class="option"
@touch="touch($event, item.url)"
>
<Label class="ico" :text="icon[item.icon]" />
<Label col="1" :text="item.title | L" class="info" />
</GridLayout>
</v-template>
</ListView>
<GridLayout row="1" class="appbar" rows="*" columns="auto, *">
<Button
class="ico"
:text="icon.back"
@tap="$navigateBack()"
/>
</GridLayout>
</GridLayout>
</Page>
</template>
<script>
import { Observable, Utils } from "@nativescript/core";
import { mapState } from "vuex";
export default {
computed: {
...mapState(["icon"]),
items() {
return [
{},
{
icon: "tg",
title: "joinTG",
url: "https://t.me/enrecipes",
},
{
icon: "help",
title: "guide",
url: "https://github.com/vishnuraghavb/EnRecipes/wiki/User-Guide",
},
{},
];
},
},
methods: {
onPageLoad(args) {
const page = args.object;
page.bindingContext = new Observable();
},
// HELPERS
openURL(url) {
Utils.openUrl(url);
},
touch({ object, action }, url) {
object.className = action.match(/down|move/) ? "option fade" : "option";
if (action == "up") this.openURL(url);
},
},
};
</script>

View file

@ -5,7 +5,6 @@
colSpan="2" colSpan="2"
rowSpan="2" rowSpan="2"
class="options-list" class="options-list"
@loaded="listViewLoad"
for="item in items" for="item in items"
> >
<v-template if="$index == 0"> <v-template if="$index == 0">
@ -29,11 +28,7 @@
</v-template> </v-template>
</ListView> </ListView>
<GridLayout row="1" class="appbar" rows="*" columns="auto, *"> <GridLayout row="1" class="appbar" rows="*" columns="auto, *">
<Button <Button class="ico" :text="icon.back" @tap="$navigateBack()" />
class="ico"
:text="icon.back"
@tap="$navigateBack()"
/>
</GridLayout> </GridLayout>
</GridLayout> </GridLayout>
</Page> </Page>
@ -50,6 +45,7 @@ import { localize, overrideLocale } from "@nativescript/localize";
import ActionDialog from "../modal/ActionDialog.vue"; import ActionDialog from "../modal/ActionDialog.vue";
import ConfirmDialog from "../modal/ConfirmDialog.vue"; import ConfirmDialog from "../modal/ConfirmDialog.vue";
import { mapState, mapActions } from "vuex"; import { mapState, mapActions } from "vuex";
import * as utils from "~/shared/utils";
export default { export default {
data() { data() {
@ -75,7 +71,7 @@ export default {
action: this.selectThemes, action: this.selectThemes,
}, },
{ {
icon: "l1", icon: "layout",
title: "listVM", title: "listVM",
subTitle: localize(this.layout), subTitle: localize(this.layout),
action: this.setLayoutMode, action: this.setLayoutMode,
@ -142,7 +138,7 @@ export default {
this.$showModal(ActionDialog, { this.$showModal(ActionDialog, {
props: { props: {
title: "List view mode", title: "List view mode",
list: ["Detailed", "Grid", "Simple", "Minimal"], list: ["detailed", "grid", "photogrid", "simple", "minimal"],
}, },
}).then((action) => { }).then((action) => {
if (action && action !== "Cancel" && this.layoutMode !== action) { if (action && action !== "Cancel" && this.layoutMode !== action) {

View file

@ -5,7 +5,6 @@
colSpan="2" colSpan="2"
rowSpan="2" rowSpan="2"
class="options-list" class="options-list"
@loaded="listViewLoad"
for="item in items" for="item in items"
> >
<v-template if="$index == 0"> <v-template if="$index == 0">
@ -14,15 +13,18 @@
<v-template if="item.type == 'switch'"> <v-template if="item.type == 'switch'">
<GridLayout columns="auto, *, auto" class="option"> <GridLayout columns="auto, *, auto" class="option">
<Label class="ico" :text="icon[item.icon]" /> <Label class="ico" :text="icon[item.icon]" />
<StackLayout col="1"> <StackLayout col="1" verticalAlignment="center">
<Label :text="item.title | L" class="info" /> <Label :text="item.title | L" class="info" />
<Label :text="item.subTitle | L" class="sub" /> <Label
v-if="item.subTitle"
:text="item.subTitle | L"
class="sub"
/>
</StackLayout> </StackLayout>
<Switch <Switch
:color="item.subAction ? '#ff5200' : '#858585'" :color="item.checked ? '#ff5200' : '#adb5bd'"
verticalAlignment="center"
col="2" col="2"
:checked="item.subAction" :checked="item.checked"
@checkedChange="item.action" @checkedChange="item.action"
/> />
</GridLayout> </GridLayout>
@ -31,12 +33,26 @@
<StackLayout class="listSpace"> </StackLayout> <StackLayout class="listSpace"> </StackLayout>
</v-template> </v-template>
</ListView> </ListView>
<GridLayout row="1" class="appbar" rows="*" columns="auto, *"> <GridLayout
<Button v-show="!toast"
class="ico" row="1"
:text="icon.back" class="appbar"
@tap="$navigateBack()" rows="*"
/> columns="auto, *"
>
<Button class="ico" :text="icon.back" @tap="$navigateBack()" />
</GridLayout>
<GridLayout
v-show="toast"
row="1"
colSpan="2"
class="appbar snackBar"
columns="*"
@tap="toast = null"
>
<FlexboxLayout minHeight="48" alignItems="center">
<Label class="title msg" :text="toast" />
</FlexboxLayout>
</GridLayout> </GridLayout>
</GridLayout> </GridLayout>
</Page> </Page>
@ -44,13 +60,17 @@
<script> <script>
import { ApplicationSettings, Observable } from "@nativescript/core"; import { ApplicationSettings, Observable } from "@nativescript/core";
import { localize } from "@nativescript/localize";
import { mapState, mapActions } from "vuex"; import { mapState, mapActions } from "vuex";
import * as utils from "~/shared/utils"; import * as utils from "~/shared/utils";
export default { export default {
data() {
return {
toast: null,
};
},
computed: { computed: {
...mapState(["icon", "shakeEnabled"]), ...mapState(["icon", "shakeEnabled", "mondayFirst"]),
items() { items() {
return [ return [
{}, {},
@ -59,29 +79,39 @@ export default {
icon: "shuf", icon: "shuf",
title: "sVw", title: "sVw",
subTitle: "sVwInfo", subTitle: "sVwInfo",
checked: this.shakeEnabled,
action: this.toggleShake, action: this.toggleShake,
subAction: this.shakeEnabled, },
{
type: "switch",
icon: "week",
title: "swm",
checked: this.mondayFirst,
action: this.toggleFirstDay,
}, },
{}, {},
]; ];
}, },
}, },
methods: { methods: {
...mapActions(["setShakeAction"]), ...mapActions(["setShake", "setFirstDay"]),
onPageLoad(args) { onPageLoad(args) {
const page = args.object; const page = args.object;
page.bindingContext = new Observable(); page.bindingContext = new Observable();
}, },
toggleFirstDay({ object }) {
this.setFirstDay(object.checked);
},
// SHAKE VIEW RANDOM RECIPE // SHAKE VIEW RANDOM RECIPE
toggleShake(args) { toggleShake({ object }) {
let checked = args.object.checked; let checked = object.checked;
if (checked && !utils.hasAccelerometer()) { if (checked && !utils.hasAccelerometer()) {
args.object.checked = false; checked = false;
// Toast.makeText(localize("noAccSensor"), "long").show(); this.toast = localize("noAccSensor");
} else { utils.timer(5, (val) => {
ApplicationSettings.setBoolean("shakeEnabled", checked); if (!val) this.toast = val;
this.setShakeAction(checked); });
} } else this.setShake(checked);
}, },
}, },
}; };

View file

@ -5,7 +5,6 @@
colSpan="2" colSpan="2"
rowSpan="2" rowSpan="2"
class="options-list" class="options-list"
@loaded="listViewLoad"
for="item in items" for="item in items"
> >
<v-template if="$index == 0"> <v-template if="$index == 0">
@ -28,9 +27,27 @@
</GridLayout> </GridLayout>
</v-template> </v-template>
</ListView> </ListView>
<GridLayout row="1" class="appbar" rows="*" columns="auto, *"> <GridLayout
v-show="!toast"
row="1"
class="appbar"
rows="*"
columns="auto, *"
>
<Button class="ico" :text="icon.back" @tap="$navigateBack()" /> <Button class="ico" :text="icon.back" @tap="$navigateBack()" />
</GridLayout> </GridLayout>
<GridLayout
v-show="toast"
row="1"
colSpan="2"
class="appbar snackBar"
columns="*"
@tap="toast = null"
>
<FlexboxLayout minHeight="48" alignItems="center">
<Label class="title msg" :text="toast" />
</FlexboxLayout>
</GridLayout>
</GridLayout> </GridLayout>
</Page> </Page>
</template> </template>
@ -39,8 +56,13 @@
import { Observable } from "@nativescript/core"; import { Observable } from "@nativescript/core";
import { localize } from "@nativescript/localize"; import { localize } from "@nativescript/localize";
import { mapState, mapActions } from "vuex"; import { mapState, mapActions } from "vuex";
import * as utils from "~/shared/utils";
export default { export default {
data() {
return {
toast: null,
};
},
computed: { computed: {
...mapState(["icon"]), ...mapState(["icon"]),
items() { items() {
@ -76,7 +98,10 @@ export default {
// RESET // RESET
resetListItems(listName) { resetListItems(listName) {
this.resetListItemsAction(listName); this.resetListItemsAction(listName);
// Toast.makeText(localize("restDone")).show(); this.toast = localize("restDone");
utils.timer(5, (val) => {
if (!val) this.toast = val;
});
}, },
touch({ object, action }, type) { touch({ object, action }, type) {
object.className = action.match(/down|move/) ? "option fade" : "option"; object.className = action.match(/down|move/) ? "option fade" : "option";

View file

@ -6,8 +6,7 @@
dock="top" dock="top"
rows="auto,auto" rows="auto,auto"
columns="*, auto" columns="*, auto"
paddingBottom="8" paddingBottom="24"
:class="{ topPlate: scrolled }"
> >
<StackLayout> <StackLayout>
<Label class="pageTitle" paddingBottom="8" :text="recipe.title" /> <Label class="pageTitle" paddingBottom="8" :text="recipe.title" />
@ -33,95 +32,81 @@
class="photo" class="photo"
decodeWidth="96" decodeWidth="96"
decodeHeight="96" decodeHeight="96"
@tap="viewPhoto"
/> />
<StackLayout row="1" colSpan="2" orientation="horizontal" margin="16">
<GridLayout
rows="48"
columns="auto, auto"
class="segment"
v-for="(item, index) in topmenu"
:key="index"
:class="{ select: selectedIndex == index }"
@touch="touchSelector($event, index, item.icon)"
>
<Label class="ico" :text="icon[item.icon]" />
<Label class="value" :text="item.count" col="1" />
</GridLayout>
</StackLayout>
</GridLayout> </GridLayout>
<ScrollView <AbsoluteLayout dock="bottom">
dock="bottom" <ScrollView
@loaded="onScrollLoad" width="100%"
@scroll="!toast && onScroll($event)" height="100%"
> @loaded="onScrollLoad"
<StackLayout> @scroll="!toast && onScroll($event)"
>
<StackLayout> <StackLayout>
<GridLayout rows="auto" columns="*, *"> <GridLayout rows="auto" columns="*, *">
<StackLayout class="attribute"> <StackLayout class="attribute">
<Label class="title" :text="'cui' | L" /> <Label class="title sub" :text="'cui' | L" />
<Label class="value" :text="recipe.cuisine | L" /> <Label class="value" :text="recipe.cuisine | L" />
</StackLayout> </StackLayout>
<StackLayout class="attribute" col="1"> <StackLayout class="attribute" col="1">
<Label class="title" :text="'cat' | L" /> <Label class="title sub" :text="'cat' | L" />
<Label class="value" :text="recipe.category | L" /> <Label class="value" :text="recipe.category | L" />
</StackLayout> </StackLayout>
</GridLayout> </GridLayout>
<StackLayout v-if="recipe.tags.length" class="attribute"> <StackLayout :hidden="!recipe.tags.length" class="attribute">
<Label class="title" :text="'ts' | L" /> <Label class="title sub" :text="'ts' | L" />
<Label class="value" :text="getTags(recipe.tags)" /> <Label class="value" :text="getTags(recipe.tags)" />
</StackLayout> </StackLayout>
<GridLayout rows="auto" columns="*, *"> <GridLayout rows="auto" columns="*, *">
<StackLayout class="attribute" v-if="hasTime(recipe.prepTime)"> <StackLayout
<Label class="title" :text="'prepT' | L" /> class="attribute"
:hidden="!hasTime(recipe.prepTime)"
>
<Label class="title sub" :text="'prepT' | L" />
<Label class="value" :text="formattedTime(recipe.prepTime)" /> <Label class="value" :text="formattedTime(recipe.prepTime)" />
</StackLayout> </StackLayout>
<StackLayout <StackLayout
:col="hasTime(recipe.prepTime) ? 1 : 0" :col="hasTime(recipe.prepTime) ? 1 : 0"
class="attribute" class="attribute"
v-if="hasTime(recipe.cookTime)" :hidden="!hasTime(recipe.cookTime)"
> >
<Label class="title" :text="'cookT' | L" /> <Label class="title sub" :text="'cookT' | L" />
<Label class="value" :text="formattedTime(recipe.cookTime)" /> <Label class="value" :text="formattedTime(recipe.cookTime)" />
</StackLayout> </StackLayout>
</GridLayout> </GridLayout>
<GridLayout rows="auto" columns="*, *"> <GridLayout rows="auto" columns="*, *">
<StackLayout class="attribute"> <StackLayout class="attribute">
<Label class="title" :text="'yld' | L" /> <Label class="title sub" :text="'yld' | L" />
<Label <Label
@touch="touchYield" @touch="touchYield"
class="value clickable" class="value clickable"
:text="`${positiveYieldMultiplier} ${$options.filters.L( :text="`${tempYieldQuantity} ${$options.filters.L(
recipe.yield.unit recipe.yield.unit
)}`" )}`"
/> />
</StackLayout> </StackLayout>
<StackLayout class="attribute" col="1"> <StackLayout class="attribute" col="1">
<Label class="title" :text="'Difficulty level' | L" /> <Label class="title sub" :text="'Difficulty level' | L" />
<Label class="value" :text="recipe.difficulty | L" /> <Label class="value" :text="recipe.difficulty | L" />
</StackLayout> </StackLayout>
</GridLayout> </GridLayout>
</StackLayout>
<StackLayout>
<Label
id="items"
padding="0 16"
class="sectionTitle"
v-show="recipe.ingredients.length"
:text="'ings' | L"
/>
<StackLayout @loaded="onIngsLoad"> <StackLayout @loaded="onIngsLoad">
<GridLayout <Label
rows="auto" padding="0 16"
columns="auto, *" class="sectionTitle"
:hidden="!recipe.ingredients.length"
:text="getTitleCount('ings', 'ingredients')"
/>
<StackLayout
orientation="horizontal"
v-for="(item, index) in recipe.ingredients" v-for="(item, index) in recipe.ingredients"
:key="index + 'ing'" :key="index + 'ing'"
class="ingredient" class="ingredient"
@touch="touchIngredient($event, index)" @touch="touchIngredient($event, index)"
> >
<Button class="ico min" :text="icon.done" /> <Button class="ico min" :text="icon.uncheck" />
<Label <Label
class="value tw" class="value tw"
col="1"
:text="`${ :text="`${
roundedQuantity(item.quantity) roundedQuantity(item.quantity)
? roundedQuantity(item.quantity) + ' ' ? roundedQuantity(item.quantity) + ' '
@ -132,33 +117,32 @@
: '' : ''
}${item.item}`" }${item.item}`"
/> />
</GridLayout> </StackLayout>
</StackLayout> </StackLayout>
<Label
id="steps"
padding="0 16"
v-show="recipe.instructions.length"
class="sectionTitle"
:text="'ins' | L"
/>
<StackLayout @loaded="onInsLoad"> <StackLayout @loaded="onInsLoad">
<GridLayout <Label
padding="0 16"
:hidden="!recipe.instructions.length"
class="sectionTitle"
:text="getTitleCount('inss', 'instructions')"
/>
<StackLayout
orientation="horizontal"
@touch="touchInstruction" @touch="touchInstruction"
columns="auto ,*"
v-for="(instruction, index) in recipe.instructions" v-for="(instruction, index) in recipe.instructions"
:key="index + 'ins'" :key="index + 'ins'"
class="instruction" class="instruction"
> >
<Button class="count ico min" :text="index + 1" /> <Button class="count ico min" :text="index + 1" />
<Label col="1" class="value tw" :text="instruction" /> <Label class="value tw" :text="instruction" />
</GridLayout> </StackLayout>
</StackLayout> </StackLayout>
<Label <Label
id="comb" @loaded="onCmbLoad"
padding="0 16" padding="0 16"
v-show="recipe.combinations.length" :hidden="!recipe.combinations.length"
class="sectionTitle" class="sectionTitle"
:text="'cmbs' | L" :text="getTitleCount('cmbs', 'combinations')"
/> />
<Button <Button
v-for="(combination, index) in recipe.combinations" v-for="(combination, index) in recipe.combinations"
@ -168,18 +152,45 @@
@tap="viewCombination(combination)" @tap="viewCombination(combination)"
/> />
<Label <Label
id="notes" @loaded="onNosTLoad"
padding="0 16" padding="0 16"
v-show="recipe.notes.length" :hidden="!recipe.notes.length"
class="sectionTitle" class="sectionTitle"
:text="'nos' | L" :text="getTitleCount('nos', 'notes')"
/>
<StackLayout @loaded="onNosLoad" padding="0 16"> </StackLayout>
<Label
class="dateInfo sub tw"
:text="`${$options.filters.L('Last updated')}: ${formattedDate(
recipe.lastModified
)}\n${$options.filters.L('Created')}: ${formattedDate(
recipe.created
)}`"
/> />
<StackLayout @loaded="onNosLoad" padding="0 16 72"> </StackLayout>
</StackLayout> </StackLayout>
</StackLayout> </ScrollView>
</ScrollView> <Label
class="sectionTitle sticky"
:hidden="!showTitleArr[0]"
:text="getTitleCount('ings', 'ingredients')"
/>
<Label
class="sectionTitle sticky"
:hidden="!showTitleArr[1]"
:text="getTitleCount('inss', 'instructions')"
/>
<Label
class="sectionTitle sticky"
:hidden="!showTitleArr[2]"
:text="getTitleCount('cmbs', 'combinations')"
/>
<Label
class="sectionTitle sticky"
:hidden="!showTitleArr[3]"
:text="getTitleCount('nos', 'notes')"
/>
</AbsoluteLayout>
</DockLayout> </DockLayout>
<GridLayout <GridLayout
row="1" row="1"
@loaded="onAppBarLoad" @loaded="onAppBarLoad"
@ -187,13 +198,17 @@
v-show="!toast" v-show="!toast"
columns="auto, *, auto, auto, auto, auto" columns="auto, *, auto, auto, auto, auto"
> >
<Button class="ico" :text="icon.back" @tap="$navigateBack()" /> <Button
class="ico"
:text="photoOpen ? icon.x : icon.back"
@tap="navigateBack"
/>
<Button <Button
col="2" col="2"
v-if="!filterTrylater" v-if="!filterTrylater"
class="ico" class="ico"
:text="recipe.tried ? icon.try : icon.tried" :text="recipe.tried ? icon.try : icon.tried"
@tap="toggleTrylater" @tap="toggle('tried')"
/> />
<Button <Button
col="2" col="2"
@ -206,7 +221,7 @@
col="3" col="3"
class="ico" class="ico"
:text="recipe.isFavorite ? icon.faved : icon.fav" :text="recipe.isFavorite ? icon.faved : icon.fav"
@tap="toggleFavourite" @tap="toggle('isFavorite')"
/> />
<Button <Button
col="4" col="4"
@ -226,8 +241,7 @@
<GridLayout <GridLayout
v-show="toast" v-show="toast"
row="1" row="1"
class="appbar" class="appbar snackBar"
:class="hlMsg && 'hlMsg'"
columns="*" columns="*"
@tap="toast = null" @tap="toast = null"
> >
@ -235,6 +249,13 @@
<Label class="title msg" :text="toast" /> <Label class="title msg" :text="toast" />
</FlexboxLayout> </FlexboxLayout>
</GridLayout> </GridLayout>
<AbsoluteLayout rowSpan="2">
<ImageZoom
@loaded="onImgZoomLoad"
:src="recipe.imageSrc"
class="photoviewer"
></ImageZoom>
</AbsoluteLayout>
</GridLayout> </GridLayout>
</Page> </Page>
</template> </template>
@ -242,6 +263,7 @@
<script> <script>
import { import {
Application, Application,
AndroidApplication,
ImageSource, ImageSource,
Utils, Utils,
Span, Span,
@ -249,15 +271,15 @@ import {
Label, Label,
Observable, Observable,
Screen, Screen,
CoreTypes,
} from "@nativescript/core"; } from "@nativescript/core";
import * as SocialShare from "@nativescript/social-share";
import { localize } from "@nativescript/localize"; import { localize } from "@nativescript/localize";
const intl = require("nativescript-intl"); const intl = require("nativescript-intl");
import { mapActions, mapState } from "vuex"; import { mapActions, mapState } from "vuex";
import EditRecipe from "./EditRecipe.vue"; import EditRecipe from "./EditRecipe.vue";
import ActionDialog from "./modal/ActionDialog.vue"; import ActionDialog from "./modal/ActionDialog.vue";
import PromptDialog from "./modal/PromptDialog.vue"; import PromptDialog from "./modal/PromptDialog.vue";
let toastTimer, scrollTimer; import * as utils from "~/shared/utils";
export default { export default {
props: ["filterTrylater", "recipeID"], props: ["filterTrylater", "recipeID"],
data() { data() {
@ -270,43 +292,25 @@ export default {
appbar: null, appbar: null,
ingcon: null, ingcon: null,
inscon: null, inscon: null,
cmbcon: null,
notescon: null, notescon: null,
notesT: null,
imgZoom: null,
checks: [], checks: [],
steps: [], checked: 0,
stepsDid: 0,
toast: null, toast: null,
hlMsg: false, photoOpen: false,
selectedIndex: 0, showTitleArr: [false, false, false, false],
scrollingToId: false,
scrolled: false,
}; };
}, },
computed: { computed: {
...mapState(["icon", "recipes"]), ...mapState(["icon", "recipes"]),
positiveYieldMultiplier() { tempYieldQuantity() {
return Math.abs(this.yieldMultiplier) > 0 return Math.abs(this.yieldMultiplier) > 0
? Math.abs(parseFloat(this.yieldMultiplier)) ? Math.abs(parseFloat(this.yieldMultiplier))
: 1; : 1;
}, },
topmenu() {
return [
{
icon: "items",
count: this.recipe.ingredients.length,
},
{
icon: "steps",
count: this.recipe.instructions.length,
},
{
icon: "comb",
count: this.recipe.combinations.length,
},
{
icon: "notes",
count: this.recipe.notes.length,
},
].filter((e) => e.count);
},
}, },
methods: { methods: {
...mapActions([ ...mapActions([
@ -321,9 +325,7 @@ export default {
const page = args.object; const page = args.object;
page.bindingContext = new Observable(); page.bindingContext = new Observable();
this.busy = false; this.busy = false;
setTimeout((e) => { this.setComponent("ViewRecipe");
this.setComponent("ViewRecipe");
}, 500);
if (this.yieldMultiplier == this.recipe.yield.quantity) if (this.yieldMultiplier == this.recipe.yield.quantity)
this.yieldMultiplier = this.recipe.yield.quantity; this.yieldMultiplier = this.recipe.yield.quantity;
this.keepScreenOn(true); this.keepScreenOn(true);
@ -341,31 +343,57 @@ export default {
onInsLoad({ object }) { onInsLoad({ object }) {
this.inscon = object; this.inscon = object;
}, },
onCmbLoad({ object }) {
this.cmbcon = object;
},
onNosTLoad({ object }) {
this.notesT = object;
},
onNosLoad({ object }) { onNosLoad({ object }) {
this.notescon = object; this.notescon = object;
this.createNotes(); this.createNotes();
}, },
onScrollLoad(args) { onScrollLoad(args) {
this.scrollview = args.object; this.scrollview = args.object;
}, },
onImgZoomLoad({ object }) {
this.imgZoom = object;
this.imgZoom.visibility = "collapsed";
this.imgZoom.top = 24;
this.imgZoom.left = Screen.mainScreen.widthDIPs - 112;
},
// FIX: scroll not smooth
stickyTitle({ object }) {
// let vm = this;
// function isTop(label) {
// let pos = label.getLocationRelativeTo(object).y;
// return label === vm.cmbcon || label === vm.notesT
// ? pos < 0
// : pos + 32 < 0;
// }
// const isAllFalse = (e) => e == false;
// if (this.recipe.notes.length && isTop(this.notesT)) {
// this.showTitleArr = [false, false, false, true];
// } else if (this.recipe.combinations.length && isTop(this.cmbcon)) {
// this.showTitleArr = [false, false, true, false];
// } else if (this.recipe.instructions.length && isTop(this.inscon)) {
// this.showTitleArr = [false, true, false, false];
// } else if (this.recipe.ingredients.length && isTop(this.ingcon)) {
// this.showTitleArr = [true, false, false, false];
// } else {
// this.showTitleArr = [false, false, false, false];
// }
// if (
// this.recipe.ingredients.length &&
// !this.showTitleArr[0] &&
// isTop(this.ingcon)
// ) {
// this.showTitleArr = [true, false, false, false];
// } else if (!this.showTitleArr.every(isAllFalse) && !isTop(this.ingcon)) {
// this.showTitleArr = [false, false, false, false];
// }
},
onScroll(args) { onScroll(args) {
if (!this.scrollingToId) {
let vm = this;
let h = Screen.mainScreen.heightDIPs;
function getPos(id) {
return vm.scrollview.getViewById(id).getLocationOnScreen().y + 144;
}
let items = getPos("items") <= h;
let steps = getPos("steps") <= h;
let comb = getPos("comb") <= h;
let notes = getPos("notes") <= h;
if (items) this.selectedIndex = 0;
if (steps) this.selectedIndex = 1;
if (comb) this.selectedIndex = 2;
if (notes) this.selectedIndex = 3;
}
this.scrolled = this.scrollview.verticalOffset != 0;
let scrollUp; let scrollUp;
let y = args.scrollY; let y = args.scrollY;
if (y) { if (y) {
@ -373,20 +401,35 @@ export default {
this.scrollPos = Math.abs(y); this.scrollPos = Math.abs(y);
let ab = this.appbar.translateY; let ab = this.appbar.translateY;
if (!scrollUp && ab == 0) if (!scrollUp && ab == 0)
this.animateInOut( this.appbar.animate({
250, translate: { x: 0, y: 64 },
false, duration: 250,
(val) => (this.appbar.translateY = val * 64) curve: CoreTypes.AnimationCurve.ease,
); });
else if (scrollUp && ab == 64) else if (scrollUp && ab == 64)
this.animateInOut( this.appbar.animate({
250, translate: { x: 0, y: 0 },
true, duration: 250,
(val) => (this.appbar.translateY = val * 64) curve: CoreTypes.AnimationCurve.ease,
); });
// this.stickyTitle(args);
} }
}, },
// HELPERS // HELPERS
getTitleCount(title, type) {
let count = this.recipe[type].length;
let selected = null;
switch (title) {
case "ings":
selected = this.checked;
break;
case "inss":
selected = this.stepsDid;
break;
}
let text = selected ? ` (${selected}/${count})` : ` (${count})`;
return localize(title) + text;
},
changeYield() { changeYield() {
this.$showModal(PromptDialog, { this.$showModal(PromptDialog, {
props: { props: {
@ -422,18 +465,17 @@ export default {
); );
}, },
showLastTried() { showLastTried() {
this.showToast( this.toast = `${localize("triedInfo")} ${this.niceDate(
`${localize("triedInfo")} ${this.niceDate(this.recipe.lastTried)}`, this.recipe.lastTried
5, )}`;
true utils.timer(5, (val) => {
); if (!val) this.toast = val;
});
}, },
roundedQuantity(quantity) { roundedQuantity(quantity) {
return Math.abs( return Math.abs(
Math.round( Math.round(
(quantity / this.recipe.yield.quantity) * (quantity / this.recipe.yield.quantity) * this.tempYieldQuantity * 100
this.positiveYieldMultiplier *
100
) / 100 ) / 100
); );
}, },
@ -485,38 +527,49 @@ export default {
recipe: this.recipe, recipe: this.recipe,
}); });
}, },
checkChange(obj, index) {
this.checks[index] = !this.checks[index];
obj.getChildAt(0).text = this.checks[index]
? this.icon.succ
: this.icon.done;
},
touchIngredient({ object, action }, index) { touchIngredient({ object, action }, index) {
object.className = action.match(/down|move/) object.className = action.match(/down|move/)
? "ingredient fade" ? "ingredient fade"
: "ingredient"; : "ingredient";
if (action == "up") this.checkChange(object, index); if (action == "up") this.checkChange(object, index);
}, },
clearChecks() { checkChange(obj, index) {
for (let i = 0; i < this.ingcon.getChildrenCount(); i++) { this.checks[index] = !this.checks[index];
this.ingcon.getChildAt(i).getChildAt(0).text = this.icon.done; if (this.checks[index]) {
this.checked++;
obj.getChildAt(0).text = this.icon.check;
} else {
this.checked--;
obj.getChildAt(0).text = this.icon.uncheck;
} }
}, },
stepDone(object) { clearChecks() {
let a = object; this.checked = 0;
a.className = a.className.includes("done") this.checks = [];
? "instruction" for (let i = 1; i < this.ingcon.getChildrenCount(); i++) {
: "instruction done"; this.ingcon.getChildAt(i).getChildAt(0).text = this.icon.uncheck;
}
}, },
touchInstruction({ object, action }) { touchInstruction({ object, action }) {
let hasDone = object.className.includes("done"); let hasDone = object.className.includes("done");
object.className = action.match(/down|move/) object.className = action.match(/down|move/)
? `instruction fade ${hasDone ? "done" : ""}` ? `instruction ${hasDone ? "done" : "fade"}`
: `instruction ${hasDone ? "done" : ""}`; : `instruction ${hasDone ? "done" : ""}`;
if (action == "up") this.stepDone(object); if (action == "up") this.stepDone(object);
}, },
stepDone(object) {
let a = object;
if (a.className.includes("done")) {
a.className = "instruction";
this.stepsDid--;
} else {
a.className = "instruction done";
this.stepsDid++;
}
},
clearSteps() { clearSteps() {
for (let i = 0; i < this.inscon.getChildrenCount(); i++) { this.stepsDid = 0;
for (let i = 1; i < this.inscon.getChildrenCount(); i++) {
this.inscon.getChildAt(i).className = "instruction"; this.inscon.getChildAt(i).className = "instruction";
} }
}, },
@ -536,12 +589,13 @@ export default {
viewCombination(combination) { viewCombination(combination) {
this.scrollview.scrollToVerticalOffset(0, true); this.scrollview.scrollToVerticalOffset(0, true);
this.recipe = this.recipes.filter((e) => e.id === combination)[0]; this.recipe = this.recipes.filter((e) => e.id === combination)[0];
this.recipe.ingredients.forEach((e) => this.checks.push(false));
this.clearSteps();
this.clearChecks(); this.clearChecks();
this.clearSteps();
this.recipe.ingredients.forEach(() => this.checks.push(false));
this.currentRecipeID = combination; this.currentRecipeID = combination;
this.syncCombinations(); this.syncCombinations();
this.createNotes(); this.createNotes();
this.yieldMultiplier = this.recipe.yield.quantity;
this.recipe.tried && this.recipe.lastTried && this.showLastTried(); this.recipe.tried && this.recipe.lastTried && this.showLastTried();
}, },
@ -556,15 +610,13 @@ export default {
}).then((result) => { }).then((result) => {
switch (result) { switch (result) {
case "pht": case "pht":
ImageSource.fromFile(this.recipe.imageSrc).then((res) => { ImageSource.fromFile(this.recipe.imageSrc).then((res) =>
SocialShare.shareImage(res, "Share recipe photo using"); utils.shareImage(res, localize("srpu"))
}); );
break; break;
case "rec": case "rec":
this.shareRecipe(); this.shareRecipe();
break; break;
default:
break;
} }
}); });
} else { } else {
@ -572,24 +624,24 @@ export default {
} }
}, },
shareRecipe() { shareRecipe() {
let overview = `${this.recipe.title}\n\n${localize( let overview = `${this.recipe.title}\n\n${localize("stars")}: ${
"Cuisine" this.recipe.rating
)}: ${localize(this.recipe.cuisine)}\n${localize("Category")}: ${localize( }\n${localize("cui")}: ${localize(this.recipe.cuisine)}\n${localize(
this.recipe.category "cat"
)}\n${localize("ts")}: ${this.recipe.tags.join(", ")}\n${localize( )}: ${localize(this.recipe.category)}\n${localize(
"stars" "ts"
)}: ${this.recipe.rating}\n${localize("Difficulty level")}: ${localize( )}: ${this.recipe.tags.join(", ")}\n${localize(
this.recipe.difficulty "prepT"
)}\n${localize("Preparation time")}: ${this.formattedTime( )}: ${this.formattedTime(this.recipe.prepTime)}\n${localize(
this.recipe.prepTime "cookT"
)}\n${localize("Cooking time")}: ${this.formattedTime( )}: ${this.formattedTime(this.recipe.cookTime)}\n${localize("yld")}: ${
this.recipe.cookTime this.tempYieldQuantity
)}\n`; } ${localize(this.recipe.yield.unit)}\n${localize(
"Difficulty level"
)}: ${localize(this.recipe.difficulty)}\n`;
let shareContent = overview; let shareContent = overview;
if (this.recipe.ingredients.length) { if (this.recipe.ingredients.length) {
let ingredients = `\n\n${localize("ings")} (${ let ingredients = `\n\n${localize("ings")}:\n\n`;
this.yieldMultiplier
} ${localize(this.recipe.yield.unit)}):\n\n`;
this.recipe.ingredients.forEach((e) => { this.recipe.ingredients.forEach((e) => {
ingredients += `- ${ ingredients += `- ${
e.quantity e.quantity
@ -603,19 +655,12 @@ export default {
shareContent += ingredients; shareContent += ingredients;
} }
if (this.recipe.instructions.length) { if (this.recipe.instructions.length) {
let instructions = `\n\n${localize("Instructions")}:\n\n`; let instructions = `\n\n${localize("inss")}:\n\n`;
this.recipe.instructions.forEach((e, i) => { this.recipe.instructions.forEach((e, i) => {
instructions += `${i + 1}. ${e}\n\n`; instructions += `${i + 1}. ${e}\n\n`;
}); });
shareContent += instructions; shareContent += instructions;
} }
if (this.recipe.notes.length) {
let notes = `\n${localize("nos")}:\n\n`;
this.recipe.notes.forEach((e, i) => {
notes += `${i + 1}. ${e}\n\n`;
});
shareContent += notes;
}
if (this.recipe.combinations.length) { if (this.recipe.combinations.length) {
let combinations = `\n${localize("cmbs")}:\n\n`; let combinations = `\n${localize("cmbs")}:\n\n`;
this.recipe.combinations.forEach((e, i) => { this.recipe.combinations.forEach((e, i) => {
@ -623,24 +668,19 @@ export default {
}); });
shareContent += combinations; shareContent += combinations;
} }
if (this.recipe.notes.length) {
let notes = `\n${localize("nos")}:\n\n`;
this.recipe.notes.forEach((e, i) => {
notes += `${i + 1}. ${e}\n\n`;
});
shareContent += notes;
}
let sharenote = "\n" + localize("appCrd"); let sharenote = "\n" + localize("appCrd");
shareContent += sharenote; shareContent += sharenote;
SocialShare.shareText(shareContent, "Share recipe using"); utils.shareText(shareContent, localize("sru"));
}, },
// DATA HANDLERS // DATA HANDLERS
showToast(msg, dur, hl) {
clearInterval(toastTimer);
this.hlMsg = hl;
this.toast = msg;
toastTimer = setInterval(() => {
dur--;
if (dur == 0) {
this.toast = null;
clearInterval(toastTimer);
}
}, 1000);
},
toggle(key, setDate) { toggle(key, setDate) {
this.toggleStateAction({ this.toggleStateAction({
id: this.currentRecipeID, id: this.currentRecipeID,
@ -649,18 +689,6 @@ export default {
setDate, setDate,
}); });
}, },
toggleFavourite() {
this.recipe.isFavorite
? this.showToast(localize("unfavd"), 3)
: this.showToast(localize("favd"), 3);
this.toggle("isFavorite");
},
toggleTrylater() {
this.recipe.tried
? this.showToast(localize("aTry"), 3)
: this.showToast(localize("rmTry"), 3);
this.toggle("tried");
},
recipeTried() { recipeTried() {
this.setRecipeAsTriedAction({ this.setRecipeAsTriedAction({
id: this.currentRecipeID, id: this.currentRecipeID,
@ -724,32 +752,91 @@ export default {
getTags(tags) { getTags(tags) {
return tags.join(" · "); return tags.join(" · ");
}, },
hijackBackEvent() {
AndroidApplication.on(
AndroidApplication.activityBackPressedEvent,
this.backEvent
);
},
releaseBackEvent() {
AndroidApplication.off(
AndroidApplication.activityBackPressedEvent,
this.backEvent
);
},
backEvent(args) {
if (this.photoOpen) {
args.cancel = true;
this.closePhoto();
} else this.$navigateBack();
},
viewPhoto() {
this.imgZoom.initNativeView();
this.photoOpen = true;
this.hijackBackEvent();
let pv = this.imgZoom;
pv.visibility = "visible";
let sw = Screen.mainScreen.widthDIPs;
let sh = Screen.mainScreen.heightDIPs;
pv.animate({
opacity: 1,
duration: 50,
})
.then(() =>
pv.animate({
width: sw,
height: sw,
translate: { x: 112 - sw, y: (sh - sw) / 3 },
duration: 250,
curve: CoreTypes.AnimationCurve.ease,
})
)
.then(() =>
pv.animate({
height: sh,
translate: { x: -sw + 112, y: -((sh - sw) / 6) },
duration: 250,
curve: CoreTypes.AnimationCurve.ease,
})
);
},
closePhoto() {
let pv = this.imgZoom;
let sw = Screen.mainScreen.widthDIPs;
let sh = Screen.mainScreen.heightDIPs;
pv.animate({
width: sw,
height: sw,
translate: { x: 112 - sw, y: (sh - sw) / 3 },
duration: 250,
curve: CoreTypes.AnimationCurve.ease,
})
.then(() =>
pv.animate({
width: 96,
height: 96,
translate: { x: 0, y: 0 },
duration: 250,
curve: CoreTypes.AnimationCurve.ease,
})
)
.then(() =>
pv.animate({
opacity: 0,
duration: 50,
})
)
.then(() => {
pv.visibility = "collapsed";
this.photoOpen = false;
this.releaseBackEvent();
});
},
navigateBack() {
this.photoOpen ? this.closePhoto() : this.$navigateBack();
},
//HELPERS //HELPERS
touchSelector({ object, action }, index, id) {
let selected = this.selectedIndex == index;
object.className = action.match(/down|move/)
? `segment ${selected ? "select" : "fade"}`
: `segment ${selected && "select"}`;
if (action == "up") this.scrollToId(index, id);
},
scrollToId(index, id) {
this.scrollingToId = true;
if (index != this.selectedIndex)
this.scrollview.scrollToVerticalOffset(
this.scrollview.getViewById(id).getLocationRelativeTo(this.scrollview)
.y + this.scrollview.verticalOffset,
true
);
clearInterval(scrollTimer);
scrollTimer = setInterval(() => {
if (this.selectedIndex == index) {
this.scrollingToId = false;
clearInterval(scrollTimer);
}
}, 1000);
this.selectedIndex = index;
},
touchYield({ object, action }) { touchYield({ object, action }) {
object.className = action.match(/down|move/) object.className = action.match(/down|move/)
? "value clickable fade" ? "value clickable fade"

View file

@ -1,5 +1,9 @@
<template> <template>
<Page @loaded="onPageLoad" backgroundColor="transparent" :class="appTheme"> <Page
@loaded="transparentPage"
backgroundColor="transparent"
:class="appTheme"
>
<GridLayout <GridLayout
columns="*" columns="*"
:rows="`auto, auto, ${stretch ? '*' : 'auto'}, auto`" :rows="`auto, auto, ${stretch ? '*' : 'auto'}, auto`"
@ -9,7 +13,6 @@
<ListView <ListView
rowHeight="48" rowHeight="48"
row="2" row="2"
@loaded="listViewLoad"
for="item in newList" for="item in newList"
:height="stretch ? '100%' : listHeight" :height="stretch ? '100%' : listHeight"
> >
@ -63,16 +66,6 @@ export default {
}, },
methods: { methods: {
...mapActions(["removeListItemAction"]), ...mapActions(["removeListItemAction"]),
onPageLoad(args) {
args.object._dialogFragment
.getDialog()
.getWindow()
.setBackgroundDrawable(
new android.graphics.drawable.ColorDrawable(
android.graphics.Color.TRANSPARENT
)
);
},
localized(item) { localized(item) {
if (this.title !== "lang") return localize(item); if (this.title !== "lang") return localize(item);
else return item; else return item;
@ -118,7 +111,6 @@ export default {
case "Unit": case "Unit":
removeListItem("unit", "units", "rmUInfo"); removeListItem("unit", "units", "rmUInfo");
break; break;
default:
} }
}, },
touch({ object, action }) { touch({ object, action }) {

View file

@ -1,5 +1,9 @@
<template> <template>
<Page @loaded="onPageLoad" backgroundColor="transparent" :class="appTheme"> <Page
@loaded="transparentPage"
backgroundColor="transparent"
:class="appTheme"
>
<GridLayout columns="*" rows="auto, auto, *, auto" class="modal"> <GridLayout columns="*" rows="auto, auto, *, auto" class="modal">
<Label class="title" :text="title | L" /> <Label class="title" :text="title | L" />
<StackLayout <StackLayout
@ -9,11 +13,11 @@
> >
<TextField <TextField
class="modalInput" class="modalInput"
:hint="'Search' | L" :hint="'ser' | L"
v-model="searchQuery" v-model="searchQuery"
/> />
</StackLayout> </StackLayout>
<ListView row="2" for="recipe in filteredRecipes" @loaded="listViewLoad"> <ListView row="2" for="recipe in filteredRecipes">
<v-template> <v-template>
<Label <Label
class="listItem" class="listItem"
@ -79,16 +83,6 @@ export default {
}, },
}, },
methods: { methods: {
onPageLoad(args) {
args.object._dialogFragment
.getDialog()
.getWindow()
.setBackgroundDrawable(
new android.graphics.drawable.ColorDrawable(
android.graphics.Color.TRANSPARENT
)
);
},
tapAction(recipe) { tapAction(recipe) {
this.$modal.close(recipe.id); this.$modal.close(recipe.id);
}, },

View file

@ -1,5 +1,9 @@
<template> <template>
<Page @loaded="onPageLoad" backgroundColor="transparent" :class="appTheme"> <Page
@loaded="transparentPage"
backgroundColor="transparent"
:class="appTheme"
>
<GridLayout rows="auto, auto, auto" class="modal"> <GridLayout rows="auto, auto, auto" class="modal">
<Label class="title" :text="title | L" /> <Label class="title" :text="title | L" />
<Label <Label
@ -8,23 +12,16 @@
class="description tw" class="description tw"
:text="description" :text="description"
/> />
<GridLayout row="2" columns="auto, *, auto, auto" class="actions"> <GridLayout row="2" columns="*, auto, auto" class="actions">
<Button
v-if="secondButtonText"
col="0"
class="text sm"
:text="secondButtonText | L"
@tap="$modal.close(-1)"
/>
<Button <Button
v-if="cancelButtonText" v-if="cancelButtonText"
col="2" col="1"
class="text sm" class="text sm"
:text="cancelButtonText | L" :text="cancelButtonText | L"
@tap="$modal.close(false)" @tap="$modal.close(false)"
/> />
<Button <Button
col="3" col="2"
class="text sm" class="text sm"
:text="okButtonText | L" :text="okButtonText | L"
@tap="$modal.close(true)" @tap="$modal.close(true)"
@ -37,27 +34,9 @@
<script> <script>
import { mapState } from "vuex"; import { mapState } from "vuex";
export default { export default {
props: [ props: ["title", "description", "cancelButtonText", "okButtonText"],
"title",
"description",
"secondButtonText",
"cancelButtonText",
"okButtonText",
],
computed: { computed: {
...mapState(["icon", "appTheme"]), ...mapState(["icon", "appTheme"]),
}, },
methods: {
onPageLoad(args) {
args.object._dialogFragment
.getDialog()
.getWindow()
.setBackgroundDrawable(
new android.graphics.drawable.ColorDrawable(
android.graphics.Color.TRANSPARENT
)
);
},
},
}; };
</script> </script>

View file

@ -20,12 +20,7 @@
</GridLayout> </GridLayout>
</StackLayout> </StackLayout>
</ScrollView> </ScrollView>
<ListView <ListView row="2" class="options-list" for="item in filterList">
row="2"
class="options-list"
@loaded="listViewLoad"
for="item in filterList"
>
<v-template> <v-template>
<Label <Label
class="listItem" class="listItem"
@ -98,10 +93,8 @@ export default {
switch (this.filterType) { switch (this.filterType) {
case "cuisine": case "cuisine":
return arr.slice(0, -2); return arr.slice(0, -2);
break;
case "category": case "category":
return arr.slice(0, -1); return arr.slice(0, -1);
break;
} }
return arr; return arr;
}, },
@ -109,13 +102,10 @@ export default {
switch (this.filterType) { switch (this.filterType) {
case "cuisine": case "cuisine":
return this.cuisineList; return this.cuisineList;
break;
case "category": case "category":
return this.categoryList; return this.categoryList;
break;
case "tag": case "tag":
return this.tagList; return this.tagList;
break;
} }
}, },
cuisineList() { cuisineList() {
@ -181,19 +171,12 @@ export default {
"clearFilter", "clearFilter",
]), ]),
onPageLoad(args) { onPageLoad(args) {
args.object._dialogFragment this.transparentPage(args);
.getDialog()
.getWindow()
.setBackgroundDrawable(
new android.graphics.drawable.ColorDrawable(
android.graphics.Color.TRANSPARENT
)
);
this.localCuisine = this.selectedCuisine; this.localCuisine = this.selectedCuisine;
this.localCategory = this.selectedCategory; this.localCategory = this.selectedCategory;
this.localTag = this.selectedTag; this.localTag = this.selectedTag;
if (this.localCuisine) this.filterType = "category"; if (this.localCuisine) this.filterType = "category";
if (this.localCategory || this.localTag) this.filterType = "tag"; if (this.localCategory && this.localTag) this.filterType = "tag";
this.scrollToRight(); this.scrollToRight();
}, },
onScrollLoad(args) { onScrollLoad(args) {
@ -209,8 +192,6 @@ export default {
case "category": case "category":
this.localTag = null; this.localTag = null;
break; break;
default:
break;
} }
}, },
scrollToRight() { scrollToRight() {

View file

@ -1,5 +1,9 @@
<template> <template>
<Page @loaded="onPageLoad" backgroundColor="transparent" :class="appTheme"> <Page
@loaded="transparentPage"
backgroundColor="transparent"
:class="appTheme"
>
<GridLayout rows="auto, auto, auto" class="modal"> <GridLayout rows="auto, auto, auto" class="modal">
<Label class="title" :text="title | L" /> <Label class="title" :text="title | L" />
<StackLayout <StackLayout
@ -9,12 +13,14 @@
horizontalAlignment="center" horizontalAlignment="center"
> >
<ListPicker <ListPicker
@loaded="onLPLoad"
ref="hrPicker" ref="hrPicker"
:items="hrsList" :items="hrsList"
:selectedIndex="hrIndex" :selectedIndex="hrIndex"
@selectedIndexChange="setHrs" @selectedIndexChange="setHrs"
></ListPicker> ></ListPicker>
<ListPicker <ListPicker
@loaded="onLPLoad"
ref="minPicker" ref="minPicker"
:items="minsList" :items="minsList"
:selectedIndex="minIndex" :selectedIndex="minIndex"
@ -40,7 +46,6 @@
</template> </template>
<script> <script>
import { Application } from "@nativescript/core";
import { mapState } from "vuex"; import { mapState } from "vuex";
import { localize } from "@nativescript/localize"; import { localize } from "@nativescript/localize";
export default { export default {
@ -81,15 +86,8 @@ export default {
}, },
}, },
methods: { methods: {
onPageLoad(args) { onLPLoad({ object }) {
args.object._dialogFragment object.android.setWrapSelectorWheel(true);
.getDialog()
.getWindow()
.setBackgroundDrawable(
new android.graphics.drawable.ColorDrawable(
android.graphics.Color.TRANSPARENT
)
);
}, },
setHrs(args) { setHrs(args) {
let hr = "0" + this.hrs[args.object.selectedIndex]; let hr = "0" + this.hrs[args.object.selectedIndex];

View file

@ -1,5 +1,9 @@
<template> <template>
<Page @loaded="onPageLoad" backgroundColor="transparent" :class="appTheme"> <Page
@loaded="transparentPage"
backgroundColor="transparent"
:class="appTheme"
>
<GridLayout rows="auto, auto, auto" class="modal"> <GridLayout rows="auto, auto, auto" class="modal">
<Label class="title" :text="title | L" /> <Label class="title" :text="title | L" />
<StackLayout row="1" class="input"> <StackLayout row="1" class="input">
@ -43,16 +47,6 @@ export default {
...mapState(["icon", "appTheme"]), ...mapState(["icon", "appTheme"]),
}, },
methods: { methods: {
onPageLoad(args) {
args.object._dialogFragment
.getDialog()
.getWindow()
.setBackgroundDrawable(
new android.graphics.drawable.ColorDrawable(
android.graphics.Color.TRANSPARENT
)
);
},
focusField({ object }) { focusField({ object }) {
let a = this.placeholder; let a = this.placeholder;
typeof a == "number" typeof a == "number"

Binary file not shown.

View file

@ -175,7 +175,6 @@
"nwCat": "New category", "nwCat": "New category",
"req": "Required", "req": "Required",
"recPic": "Recipe photo", "recPic": "Recipe photo",
"repBtn": "REPLACE",
"cPic": "Crop photo", "cPic": "Crop photo",
"breakfast": "Breakfast", "breakfast": "Breakfast",
"lunch": "lunch", "lunch": "lunch",
@ -271,7 +270,6 @@
"buInc": "Malformed or corrupt backup file", "buInc": "Malformed or corrupt backup file",
"buEmp": "The backup file is empty", "buEmp": "The backup file is empty",
"buMod": "The backup file was modified elsewhere", "buMod": "The backup file was modified elsewhere",
"buSuc": "Backup saved successfully to the Download folder",
"invFile": "Invalid file", "invFile": "Invalid file",
"donate": "Donate", "donate": "Donate",
"appInfo": "EnRecipes is an open source, privacy-friendly digital cookbook that lets you create, manage and share your recipes", "appInfo": "EnRecipes is an open source, privacy-friendly digital cookbook that lets you create, manage and share your recipes",
@ -308,5 +306,11 @@
"apply": "APPLY", "apply": "APPLY",
"fltr": "Filter", "fltr": "Filter",
"yld": "Yield", "yld": "Yield",
"swm": "Start week on Monday" "swm": "Start week on Monday",
"expip": "Export in progress",
"impip": "Import in progress",
"srpu": "Share recipe photo using...",
"sru": "Share recipe using...",
"aap": "Attach a photo",
"rp": "Remove photo"
} }

View file

@ -15,10 +15,15 @@ import store from './store'
import CollectionView from '@nativescript-community/ui-collectionview/vue' import CollectionView from '@nativescript-community/ui-collectionview/vue'
Vue.use(CollectionView) Vue.use(CollectionView)
Vue.registerElement(
'ImageZoom',
() => require('@triniwiz/nativescript-image-zoom').ImageZoom
)
import { lvMixin } from './shared/mixins.js' import { lvMixin } from './shared/mixins.js'
Vue.mixin(lvMixin) Vue.mixin(lvMixin)
Vue.config.silent = TNS_ENV === 'production' Vue.config.silent = false
Vue.filter('L', localize) Vue.filter('L', localize)

View file

@ -18,7 +18,9 @@ android {
versionCode 7 versionCode 7
versionName '1.3.2' versionName '1.3.2'
applicationId 'com.vishnuraghav.enrecipes' applicationId 'com.vishnuraghav.enrecipes'
minSdkVersion 22 minSdkVersion 23
targetSdkVersion 30
compileSdkVersion 30
generatedDensities = [] generatedDensities = []
ndk { ndk {
abiFilters.clear() abiFilters.clear()

View file

@ -2,14 +2,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="__PACKAGE__" android:versionCode="10000" android:versionName="1.0"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="__PACKAGE__" android:versionCode="10000" android:versionName="1.0">
<supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" /> <supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" />
<uses-permission android:name="android.permission.CAMERA" tools:node="remove" /> <uses-permission android:name="android.permission.CAMERA" tools:node="remove" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" tools:node="remove" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" tools:node="remove" />
<uses-permission android:name="android.permission.INTERNET" tools:node="remove" /> <uses-permission android:name="android.permission.INTERNET" tools:node="remove" />
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" /> <uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />
<application android:name="com.tns.NativeScriptApplication" android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:hardwareAccelerated="true" android:largeHeap="true" android:theme="@style/AppTheme" android:requestLegacyExternalStorage="true"> <application android:name="com.tns.NativeScriptApplication" android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:hardwareAccelerated="true" android:largeHeap="true" android:theme="@style/AppTheme" android:preserveLegacyExternalStorage="true">
<activity android:name="com.tns.NativeScriptActivity" android:label="@string/title_activity_kimera" android:screenOrientation="userPortrait" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|uiMode" android:theme="@style/LaunchScreenTheme" android:windowSoftInputMode="adjustResize"> <activity android:name="com.tns.NativeScriptActivity" android:label="@string/title_activity_kimera" android:screenOrientation="userPortrait" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|uiMode" android:theme="@style/LaunchScreenTheme" android:windowSoftInputMode="adjustResize">
<meta-data android:name="SET_THEME_ON_LAUNCH" android:resource="@style/AppTheme" /> <meta-data android:name="SET_THEME_ON_LAUNCH" android:resource="@style/AppTheme" />
<intent-filter> <intent-filter>

View file

@ -1,20 +1,24 @@
export const lvMixin = { export const lvMixin = {
methods: { methods: {
listViewLoad(args) { transparentPage({ object }) {
let e = args.object.android object._dialogFragment
e.setSelector(new android.graphics.drawable.StateListDrawable()) .getDialog()
e.setDivider(null) .getWindow()
e.setDividerHeight(0) .setBackgroundDrawable(
}, new android.graphics.drawable.ColorDrawable(
animateInOut(dur, rev, draw) { android.graphics.Color.TRANSPARENT
const start = Date.now() )
let timer = setInterval(() => { )
const passed = Date.now() - start
let val = passed / dur
if (val > 1) val = 1
draw(rev ? 1 - val : val)
if (val === 1) clearInterval(timer)
}, 17) // 1000ms/60fps=16.66ms => 17ms
}, },
// animateInOut(dur, rev, draw) {
// const start = Date.now()
// let timer = setInterval(() => {
// const passed = Date.now() - start
// let val = passed / dur
// if (val > 1) val = 1
// draw(rev ? 1 - val : val)
// if (val === 1) clearInterval(timer)
// }, 17) // 1000ms/60fps=16.66ms => 17ms
// },
}, },
} }

View file

@ -35,8 +35,8 @@ export const openAppSettingsPage = () => {
Application.android.foregroundActivity.startActivity(intent) Application.android.foregroundActivity.startActivity(intent)
} }
export const hasAccelerometer = () => { export const hasAccelerometer = () => {
let context = Utils.ad.getApplicationContext() let ctx = Utils.ad.getApplicationContext()
let sensorManager = context.getSystemService( let sensorManager = ctx.getSystemService(
android.content.Context.SENSOR_SERVICE android.content.Context.SENSOR_SERVICE
) )
return sensorManager.getDefaultSensor( return sensorManager.getDefaultSensor(
@ -62,9 +62,8 @@ export const timer = (dur, callback) => {
}, 1000) }, 1000)
} }
// FOLDER PICKER function callIntent(ctx, intent, msg, pickerType) {
function callIntent(context, intent, pickerType) { return new Promise((resolve) => {
return new Promise((resolve, reject) => {
const onEvent = function(e) { const onEvent = function(e) {
if (e.requestCode === pickerType) { if (e.requestCode === pickerType) {
resolve(e) resolve(e)
@ -72,217 +71,65 @@ function callIntent(context, intent, pickerType) {
} }
} }
Application.android.once(AndroidApplication.activityResultEvent, onEvent) Application.android.once(AndroidApplication.activityResultEvent, onEvent)
context.startActivityForResult(intent, pickerType) ctx.startActivityForResult(
android.content.Intent.createChooser(intent, msg),
pickerType
)
})
}
// IMAGE PICKER
export const getRecipePhoto = () => {
const ctx =
Application.android.foregroundActivity || Application.android.startActivity
const DIR_CODE = Math.round(Math.random() * 10000)
const intent = new android.content.Intent(
android.content.Intent.ACTION_GET_CONTENT
)
intent.setType('image/*')
return callIntent(ctx, intent, 'Select photo', DIR_CODE).then((res) => {
if (res.resultCode === android.app.Activity.RESULT_OK)
if (res.intent != null && res.intent.getData())
return res.intent.getData()
})
}
export const copyPhotoToCache = (uri, filepath) => {
const ContentResolver = Application.android.nativeApp.getContentResolver()
return new Promise((resolve) => {
const inputStream = ContentResolver.openInputStream(uri)
const input = new java.io.BufferedInputStream(inputStream)
let size = input.available()
let buffer = Array.create('byte', size)
const output = new java.io.BufferedOutputStream(
new java.io.FileOutputStream(filepath)
)
input.read(buffer)
do {
output.write(buffer)
} while (input.read(buffer) != -1)
input.close()
output.close()
resolve(filepath)
}) })
} }
//SOURCE: https://github.com/NativeScript/nativescript-imagepicker/blob/bc9209dd7d773eb437ff812f07873c72c789d572/src/imagepicker.android.ts // BACKUP FOLDER PICKER
export const getBackupFolder = () => {
function getDataColumn(uri, selection, selectionArgs, isDownload) { const ctx =
const ContentResolver = Application.android.nativeApp.getContentResolver()
let cursor = null
let filePath
if (isDownload) {
let columns = ['_display_name']
try {
cursor = ContentResolver.query(
uri,
columns,
selection,
selectionArgs,
null
)
if (cursor != null && cursor.moveToFirst()) {
let column_index = cursor.getColumnIndexOrThrow(columns[0])
filePath = cursor.getString(column_index)
if (filePath) {
const dl = android.os.Environment.getExternalStoragePublicDirectory(
android.os.Environment.DIRECTORY_DOWNLOADS
)
filePath = `${dl}/${filePath}`
return filePath
}
}
} catch (e) {
console.log(e)
} finally {
if (cursor) {
cursor.close()
}
}
} else {
let columns = [android.provider.MediaStore.MediaColumns.DATA]
let filePath
try {
cursor = ContentResolver.query(
uri,
columns,
selection,
selectionArgs,
null
)
if (cursor != null && cursor.moveToFirst()) {
let column_index = cursor.getColumnIndexOrThrow(columns[0])
filePath = cursor.getString(column_index)
if (filePath) {
return filePath
}
}
} catch (e) {
console.log(e)
} finally {
if (cursor) {
cursor.close()
}
}
}
return undefined
}
function getTreeUri(uri) {
const DocumentsContract = android.provider.DocumentsContract
const ContentResolver = Application.android.nativeApp.getContentResolver()
if (DocumentsContract.isTreeUri(uri)) {
let docId, id, type
let contentUri = null
if ('com.android.externalstorage.documents' === uri.getAuthority()) {
docId = DocumentsContract.getTreeDocumentId(uri).split(':')
type = docId[0]
id = docId[1]
if ('primary' === type.toLowerCase())
return android.os.Environment.getExternalStorageDirectory() + '/' + id
else {
ContentResolver.takePersistableUriPermission(
uri,
android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION |
android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
const externalMediaDirs = Application.android.context.getExternalMediaDirs()
if (externalMediaDirs.length > 1) {
let filePath = externalMediaDirs[1].getAbsolutePath()
filePath = filePath.substring(0, filePath.indexOf('Android')) + id
return filePath
}
}
}
// DownloadsProvider
else if (
'com.android.providers.downloads.documents' === uri.getAuthority()
) {
return getDataColumn(uri, null, null, true)
}
// MediaProvider
else if ('com.android.providers.media.documents' === uri.getAuthority()) {
docId = DocumentsContract.getTreeDocumentId(uri).split(':')
type = docId[0]
id = docId[1]
if ('image' === type)
contentUri =
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
else if ('video' === type)
contentUri =
android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI
else if ('audio' === type)
contentUri =
android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
let selection = '_id=?'
let selectionArgs = [id]
return getDataColumn(contentUri, selection, selectionArgs, false)
}
}
return undefined
}
function getFileUri(uri) {
const DocumentsContract = android.provider.DocumentsContract
const ContentResolver = Application.android.nativeApp.getContentResolver()
if (DocumentsContract.isDocumentUri(Application.android.context, uri)) {
let docId, id, type
let contentUri = null
if ('com.android.externalstorage.documents' === uri.getAuthority()) {
docId = DocumentsContract.getDocumentId(uri).split(':')
type = docId[0]
id = docId[1]
if ('primary' === type.toLowerCase())
return android.os.Environment.getExternalStorageDirectory() + '/' + id
else {
ContentResolver.takePersistableUriPermission(
uri,
android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION |
android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
const externalMediaDirs = Application.android.context.getExternalMediaDirs()
if (externalMediaDirs.length > 1) {
let filePath = externalMediaDirs[1].getAbsolutePath()
filePath = filePath.substring(0, filePath.indexOf('Android')) + id
return filePath
}
}
}
// DownloadsProvider
else if (
'com.android.providers.downloads.documents' === uri.getAuthority()
) {
return getDataColumn(uri, null, null, true)
}
// MediaProvider
else if ('com.android.providers.media.documents' === uri.getAuthority()) {
docId = DocumentsContract.getDocumentId(uri).split(':')
type = docId[0]
id = docId[1]
if ('image' === type)
contentUri =
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
else if ('video' === type)
contentUri =
android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI
else if ('audio' === type)
contentUri =
android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
let selection = '_id=?'
let selectionArgs = [id]
return getDataColumn(contentUri, selection, selectionArgs, false)
}
}
return undefined
}
export const pickFolder = () => {
const context =
Application.android.foregroundActivity || Application.android.startActivity Application.android.foregroundActivity || Application.android.startActivity
const DIR_CODE = Math.round(Math.random() * 10000) const DIR_CODE = Math.round(Math.random() * 10000)
const intent = new android.content.Intent( const intent = new android.content.Intent(
android.content.Intent.ACTION_OPEN_DOCUMENT_TREE android.content.Intent.ACTION_OPEN_DOCUMENT_TREE
) )
intent.addCategory(android.content.Intent.CATEGORY_DEFAULT) return callIntent(ctx, intent, 'Select folder', DIR_CODE).then((res) => {
return callIntent(context, intent, DIR_CODE).then((res) => {
if (res.resultCode === android.app.Activity.RESULT_OK) if (res.resultCode === android.app.Activity.RESULT_OK)
if (res.intent != null && res.intent.getData()) if (res.intent != null && res.intent.getData())
return getTreeUri(res.intent.getData()) return res.intent.getData()
}) })
} }
function callBackupIntent(context, intent, pickerType) { // BACKUP FILE PICKER
return new Promise((resolve, reject) => {
const onEvent = function(e) {
if (e.requestCode === pickerType) {
resolve(e)
Application.android.off(AndroidApplication.activityResultEvent, onEvent)
}
}
Application.android.once(AndroidApplication.activityResultEvent, onEvent)
context.startActivityForResult(intent, pickerType)
})
}
export const getBackupFile = () => { export const getBackupFile = () => {
const context = const ctx =
Application.android.foregroundActivity || Application.android.startActivity Application.android.foregroundActivity || Application.android.startActivity
const DIR_CODE = Math.round(Math.random() * 10000) const DIR_CODE = Math.round(Math.random() * 10000)
const intent = new android.content.Intent( const intent = new android.content.Intent(
@ -290,13 +137,137 @@ export const getBackupFile = () => {
) )
intent.addCategory(android.content.Intent.CATEGORY_OPENABLE) intent.addCategory(android.content.Intent.CATEGORY_OPENABLE)
intent.setType('application/zip') intent.setType('application/zip')
// intent.setFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION) return callIntent(ctx, intent, 'Select file to import', DIR_CODE).then(
return callBackupIntent(context, intent, DIR_CODE).then((res) => { (res) => {
if (res.resultCode === android.app.Activity.RESULT_OK) { if (res.resultCode === android.app.Activity.RESULT_OK) {
if (res.intent != null && res.intent.getData()) { if (res.intent != null && res.intent.getData())
// return getFileUri(res.intent.getData()) return res.intent.getData()
return res.intent.getData()
} }
} }
}) )
}
// ZIP OPERATIONS
export class Zip {
static getSubFiles(src, isRootFolder) {
const fileList = new java.util.ArrayList()
const sourceFile = new java.io.File(src)
let tempList = sourceFile.listFiles()
for (let i = 0; i < tempList.length; i++) {
if (tempList[i].isFile()) {
fileList.add(tempList[i])
}
if (tempList[i].isDirectory()) {
if (isRootFolder) {
fileList.add(tempList[i])
}
fileList.addAll(Zip.getSubFiles(tempList[i].getAbsolutePath()))
}
}
return fileList
}
static zip(src, destUri, filename) {
const ContentResolver = Application.android.nativeApp.getContentResolver()
const parsedUri = new android.net.Uri.parse(destUri)
const uri = new androidx.documentfile.provider.DocumentFile.fromTreeUri(
Application.android.context,
parsedUri
)
.createFile('application/zip', filename)
.getUri()
const outputStream = ContentResolver.openOutputStream(uri)
const zipOutputStream = new java.util.zip.ZipOutputStream(outputStream)
return new Promise((resolve) => {
const sourceFiles = Zip.getSubFiles(src, true)
for (let i = 0; i < sourceFiles.size(); i++) {
let len
let buffer = Array.create('byte', 4096)
let file = sourceFiles.get(i)
let entry = new java.util.zip.ZipEntry(
'EnRecipes/' +
new java.io.File(src)
.toURI()
.relativize(file.toURI())
.getPath()
)
zipOutputStream.putNextEntry(entry)
if (!file.isDirectory()) {
let inputStream = new java.io.FileInputStream(file)
while ((len = inputStream.read(buffer)) != -1) {
zipOutputStream.write(buffer, 0, len)
}
inputStream.close()
}
zipOutputStream.closeEntry()
}
zipOutputStream.close()
resolve(uri)
})
}
static unzip(uri, dest) {
const ContentResolver = Application.android.nativeApp.getContentResolver()
return new Promise((resolve) => {
const inputStream = ContentResolver.openInputStream(uri)
const zipInputStream = new java.util.zip.ZipInputStream(inputStream)
let len, filepath
let buffer = Array.create('byte', 4096)
let entry = zipInputStream.getNextEntry()
filepath = dest + '/' + entry.getName()
while (entry != null) {
filepath = dest + '/' + entry.getName()
if (!entry.isDirectory()) {
const output = new java.io.BufferedOutputStream(
new java.io.FileOutputStream(filepath)
)
while ((len = zipInputStream.read(buffer)) != -1) {
output.write(buffer, 0, len)
}
output.close()
} else {
let dir = new java.io.File(filepath)
dir.mkdirs()
}
zipInputStream.closeEntry()
entry = zipInputStream.getNextEntry()
}
zipInputStream.close()
resolve(dest)
})
}
}
// SHARE OPERATIONS
function share(intent, subject) {
const ctx = Application.android.context
const shareIntent = android.content.Intent.createChooser(intent, subject)
shareIntent.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
ctx.startActivity(shareIntent)
}
function getSendIntent(type) {
const intent = new android.content.Intent(android.content.Intent.ACTION_SEND)
intent.setType(type)
return intent
}
export const shareText = (text, subject) => {
const intent = getSendIntent('text/plain')
intent.putExtra(android.content.Intent.EXTRA_TEXT, text)
share(intent, subject)
}
export const shareImage = (image, subject) => {
let ctx = Application.android.context
const intent = getSendIntent('image/jpeg')
const baos = new java.io.ByteArrayOutputStream()
image.android.compress(android.graphics.Bitmap.CompressFormat.JPEG, 100, baos)
const tmpFile = new java.io.File(ctx.getExternalCacheDir(), 'EnRecipes.jpg')
const fos = new java.io.FileOutputStream(tmpFile)
fos.write(baos.toByteArray())
fos.flush()
fos.close()
intent.putExtra(
android.content.Intent.EXTRA_STREAM,
android.net.Uri.fromFile(tmpFile)
)
share(intent, subject)
} }

View file

@ -160,72 +160,61 @@ export default new Vuex.Store({
units: [], units: [],
mealPlans: [], mealPlans: [],
icon: { icon: {
alert: '\ue900', back: '\ue900',
back: '\ue901', bag: '\ue901',
bag: '\ue902', bagged: '\ue902',
bagged: '\ue903', cal: '\ue903',
cal: '\ue904', category: '\ue904',
cam: '\ue905', check: '\ue905',
category: '\ue906', cog: '\ue906',
cog: '\ue907', cuisine: '\ue907',
comb: '\ue908', db: '\ue908',
cuisine: '\ue909', del: '\ue909',
del: '\ue90a', diff: '\ue90a',
diff: '\ue90b', don: '\ue90b',
don: '\ue90c', done: '\ue90c',
done: '\ue90d', edit: '\ue90d',
edit: '\ue90e', exp: '\ue90e',
exp: '\ue910', fav: '\ue90f',
fav: '\ue911', faved: '\ue910',
faved: '\ue912', filter: '\ue911',
folder: '\ue913', folder: '\ue912',
gh: '\ue914', gh: '\ue913',
help: '\ue915', help: '\ue914',
home: '\ue916', home: '\ue915',
img: '\ue917', img: '\ue916',
imp: '\ue918', imp: '\ue917',
info: '\ue919', info: '\ue918',
items: '\ue91a', interface: '\ue919',
l1: '\ue91b', lang: '\ue91a',
l2: '\ue91c', layout: '\ue91b',
l3: '\ue91d', left: '\ue91c',
lang: '\ue91e', menu: '\ue91d',
left: '\ue91f', opts: '\ue91e',
menu: '\ue920', plus: '\ue91f',
noresult: '\ue921', price: '\ue920',
notes: '\ue922', priv: '\ue921',
plus: '\ue923', reset: '\ue922',
plusc: '\ue924', right: '\ue923',
price: '\ue925', save: '\ue924',
priv: '\ue926', sear: '\ue925',
err: '\ue90f', share: '\ue926',
res: '\ue927', shuf: '\ue927',
reset: '\ue928', sort: '\ue928',
right: '\ue929', star: '\ue929',
save: '\ue92a', starred: '\ue92a',
sear: '\ue92b', tag: '\ue92b',
selall: '\ue92c', tg: '\ue92c',
share: '\ue92d', theme: '\ue92d',
shuf: '\ue92e', time: '\ue92e',
sort: '\ue92f', tod: '\ue92f',
star: '\ue930', trans: '\ue930',
starred: '\ue931', tried: '\ue931',
steps: '\ue932', try: '\ue932',
succ: '\ue933', uncheck: '\ue933',
tag: '\ue934', undo: '\ue934',
text: '\ue935', week: '\ue935',
tg: '\ue936', x: '\ue936',
theme: '\ue937',
time: '\ue938',
timer: '\ue939',
tod: '\ue93a',
trans: '\ue93b',
tried: '\ue93c',
try: '\ue93d',
unit: '\ue93e',
x: '\ue93f',
yield: '\ue940',
zip: '\ue941',
}, },
currentComponent: 'EnRecipes', currentComponent: 'EnRecipes',
sortType: 'Oldest first', sortType: 'Oldest first',
@ -298,11 +287,19 @@ export default new Vuex.Store({
selectedCategory: null, selectedCategory: null,
selectedTag: null, selectedTag: null,
appTheme: 'Light', appTheme: 'Light',
mondayFirst: false,
}, },
mutations: { mutations: {
clearImportSummary(state) {
for (const key in state.importSummary) state.importSummary[key] = 0
},
setFirstDay(state, bool) {
state.mondayFirst = bool
ApplicationSettings.setBoolean('mondayFirst', bool)
},
setTheme(state, theme) { setTheme(state, theme) {
ApplicationSettings.setString('appTheme', theme)
state.appTheme = theme state.appTheme = theme
ApplicationSettings.setString('appTheme', theme)
}, },
clearFilter(state) { clearFilter(state) {
state.selectedCuisine = state.selectedCategory = state.selectedTag = null state.selectedCuisine = state.selectedCategory = state.selectedTag = null
@ -671,6 +668,7 @@ export default new Vuex.Store({
}, },
setShake(state, shake) { setShake(state, shake) {
state.shakeEnabled = shake state.shakeEnabled = shake
ApplicationSettings.setBoolean('shakeEnabled', shake)
}, },
setRating(state, { id, recipe, rating }) { setRating(state, { id, recipe, rating }) {
let index = state.recipes.indexOf( let index = state.recipes.indexOf(
@ -706,6 +704,12 @@ export default new Vuex.Store({
}, },
}, },
actions: { actions: {
clearImportSummary({ commit }) {
commit('clearImportSummary')
},
setFirstDay({ commit }, bool) {
commit('setFirstDay', bool)
},
setTheme({ commit }, theme) { setTheme({ commit }, theme) {
commit('setTheme', theme) commit('setTheme', theme)
}, },
@ -715,7 +719,7 @@ export default new Vuex.Store({
setLayout({ commit }, type) { setLayout({ commit }, type) {
commit('setLayout', type) commit('setLayout', type)
}, },
setSortTypeAction({ commit }, sortType) { setSortType({ commit }, sortType) {
commit('setSortType', sortType) commit('setSortType', sortType)
}, },
initRecipes({ commit }) { initRecipes({ commit }) {
@ -778,7 +782,7 @@ export default new Vuex.Store({
unSyncCombinationsAction({ commit }, combinations) { unSyncCombinationsAction({ commit }, combinations) {
commit('unSyncCombinations', combinations) commit('unSyncCombinations', combinations)
}, },
setShakeAction({ commit }, shake) { setShake({ commit }, shake) {
commit('setShake', shake) commit('setShake', shake)
}, },
setRatingAction({ commit }, rating) { setRatingAction({ commit }, rating) {

View file

@ -5,4 +5,5 @@
- Improvement: Faster app start and navigation - Improvement: Faster app start and navigation
- Added app languages: French, Italian, Norwegian Bokmål - Added app languages: French, Italian, Norwegian Bokmål
- Updated translations - Updated translations
- Dropped support for Android Lollipop
- Several bug fixes - Several bug fixes

View file

@ -2,7 +2,8 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"~/*": ["app/*"] "~/*": ["app/*"],
"@/*": ["app/*"]
} }
}, },
"include": ["app/**/*"] "include": ["app/**/*"]

View file

@ -7,5 +7,6 @@ export default {
android: { android: {
v8Flags: "--expose_gc", v8Flags: "--expose_gc",
markingMode: "none", markingMode: "none",
} },
// profiling: 'timeline'
} as NativeScriptConfig } as NativeScriptConfig

18800
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "enrecipes", "name": "enrecipes",
"main": "main.js", "main": "./app/main.js",
"version": "1.0.0", "version": "1.0.0",
"description": "A native application built with NativeScript-Vue", "description": "A native application built with NativeScript-Vue",
"homepage": "https://enrecipes.vercel.app/", "homepage": "https://enrecipes.vercel.app/",
@ -18,27 +18,21 @@
}, },
"private": "true", "private": "true",
"dependencies": { "dependencies": {
"@nativescript-community/perms": "^2.1.5",
"@nativescript-community/ui-collectionview": "^4.0.29", "@nativescript-community/ui-collectionview": "^4.0.29",
"@nativescript/core": "7.3.0", "@nativescript/core": "~8.0.1",
"@nativescript/localize": "^5.0.4", "@nativescript/localize": "^5.0.4",
"@nativescript/social-share": "^2.0.4",
"@nativescript/zip": "^5.0.0",
"@triniwiz/nativescript-accelerometer": "^4.0.3", "@triniwiz/nativescript-accelerometer": "^4.0.3",
"@triniwiz/nativescript-couchbase": "^1.2.2", "@triniwiz/nativescript-couchbase": "^1.2.2",
"@triniwiz/nativescript-image-zoom": "^4.0.0",
"nativescript-imagecropper": "^4.0.1", "nativescript-imagecropper": "^4.0.1",
"nativescript-intl": "^4.0.2", "nativescript-intl": "^4.0.2",
"nativescript-vue": "^2.8.4", "nativescript-vue": "~2.9.0",
"vuex": "^3.6.2" "vuex": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.10", "@nativescript/android": "8.0.0",
"@babel/preset-env": "^7.12.11", "@nativescript/webpack": "beta",
"@nativescript/android": "7.0.1", "nativescript-vue-template-compiler": "~2.9.0",
"@nativescript/webpack": "4.1.0", "sass": "^1.32.8"
"babel-loader": "^8.2.2",
"nativescript-vue-template-compiler": "^2.8.3",
"node-sass": "^4.14.1",
"vue-loader": "^15.9.6"
} }
} }