optimized recipe editing

This commit is contained in:
Vishnu Raghav B 2020-11-02 17:06:53 +05:30
parent 399fc6bc20
commit efc254425d
50 changed files with 3220 additions and 2607 deletions

View file

@ -15,6 +15,8 @@
android {
defaultConfig {
versionCode 1
versionName '1.0.0'
minSdkVersion 25
generatedDensities = []
ndk {

View file

@ -7,8 +7,8 @@
<!-- <uses-permission android:name="android.permission.READ_USER_DICTIONARY" /> -->
<!-- <uses-permission android:name="android.permission.INTERNET"/> -->
<!-- <uses-feature android:name="android.hardware.camera" android:required="true" /> -->
<application android:usesCleartextTraffic="true" android:name="com.tns.NativeScriptApplication" android:allowBackup="true" android:icon="@drawable/icon" android:label="@string/app_name" android:theme="@style/AppTheme" android:requestLegacyExternalStorage="true">
<activity android:name="com.tns.NativeScriptActivity" android:label="@string/title_activity_kimera" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|uiMode" android:theme="@style/LaunchScreenTheme">
<application android:name="com.tns.NativeScriptApplication" android:allowBackup="true" android:icon="@drawable/icon" android:label="@string/app_name" android:theme="@style/AppTheme" android:requestLegacyExternalStorage="true">
<activity android:name="com.tns.NativeScriptActivity" android:label="@string/title_activity_kimera" android:screenOrientation="portrait" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|locale|uiMode" android:theme="@style/LaunchScreenTheme" android:windowSoftInputMode="adjustPan">
<meta-data android:name="SET_THEME_ON_LAUNCH" android:resource="@style/AppTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

Before

Width:  |  Height:  |  Size: 550 B

After

Width:  |  Height:  |  Size: 550 B

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View file

Before

Width:  |  Height:  |  Size: 461 B

After

Width:  |  Height:  |  Size: 461 B

View file

Before

Width:  |  Height:  |  Size: 664 B

After

Width:  |  Height:  |  Size: 664 B

View file

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 472 B

View file

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 780 B

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

Before

Width:  |  Height:  |  Size: 680 B

After

Width:  |  Height:  |  Size: 680 B

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 861 B

After

Width:  |  Height:  |  Size: 861 B

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -3,7 +3,7 @@
<!-- Application theme -->
<style name="AppThemeBase29" parent="AppThemeBase21">
<item name="android:forceDarkAllowed">true</item>
<item name="android:forceDarkAllowed">false</item>
</style>
<style name="AppTheme" parent="AppThemeBase29">

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- theme to use FOR launch screen -->
<style name="LaunchScreenThemeBase" parent="Theme.AppCompat.Light.NoActionBar">
<item name="toolbarStyle">
@style/NativeScriptToolbarStyle
</item>
<item name="colorPrimary">
@color/ns_primary
</item>
<item name="colorPrimaryDark">
@color/ns_primaryDark
</item>
<item name="colorAccent">
@color/ns_accent
</item>
<item name="android:windowBackground">
@drawable/splash_screen
</item>
<item name="android:windowActionBarOverlay">
true
</item>
<item name="android:windowTranslucentStatus">
true
</item>
</style>
<style name="LaunchScreenTheme" parent="LaunchScreenThemeBase">
</style>
<!-- theme to use AFTER launch screen is loaded -->
<style name="AppThemeBase" parent="Theme.AppCompat.Light.NoActionBar">
<item name="toolbarStyle">
@style/NativeScriptToolbarStyle
</item>
<item name="colorPrimary">
@color/ns_primary
</item>
<item name="colorPrimaryDark">
@color/ns_primaryDark
</item>
<item name="colorAccent">
@color/ns_accent
</item>
</style>
<style name="AppTheme" parent="AppThemeBase">
</style>
<!-- theme for action-bar -->
<style name="NativeScriptToolbarStyleBase" parent="Widget.AppCompat.Toolbar">
<item name="android:background">
@color/ns_primary
</item>
<item name="theme">
@style/ThemeOverlay.AppCompat.ActionBar
</item>
<item name="popupTheme">
@style/ThemeOverlay.AppCompat
</item>
</style>
<style name="NativeScriptToolbarStyle" parent="NativeScriptToolbarStyleBase">
</style>
</resources>

View file

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- theme to use FOR launch screen-->
<style name="LaunchScreenThemeBase" parent="Theme.AppCompat.Light.NoActionBar">
<item name="toolbarStyle">@style/NativeScriptToolbarStyle</item>
<item name="colorPrimary">@color/ns_primary</item>
<item name="colorPrimaryDark">@color/ns_primaryDark</item>
<item name="colorAccent">@color/ns_accent</item>
<item name="android:windowBackground">@drawable/splash_screen</item>
<item name="android:windowActionBarOverlay">true</item>
<item name="android:windowTranslucentStatus">false</item>
</style>
<style name="LaunchScreenTheme" parent="LaunchScreenThemeBase">
</style>
<!-- theme to use AFTER launch screen is loaded-->
<style name="AppThemeBase" parent="Theme.AppCompat.Light.NoActionBar">
<item name="toolbarStyle">@style/NativeScriptToolbarStyle</item>
<item name="colorPrimary">@color/ns_primary</item>
<item name="colorPrimaryDark">@color/ns_primaryDark</item>
<item name="colorAccent">@color/ns_accent</item>
</style>
<style name="AppTheme" parent="AppThemeBase">
</style>
<!-- theme for action-bar -->
<style name="NativeScriptToolbarStyleBase" parent="Widget.AppCompat.Toolbar">
<item name="android:background">@color/ns_primary</item>
<item name="theme">@style/ThemeOverlay.AppCompat.ActionBar</item>
<item name="popupTheme">@style/ThemeOverlay.AppCompat</item>
</style>
<style name="NativeScriptToolbarStyle" parent="NativeScriptToolbarStyleBase">
</style>
</resources>

View file

@ -13,13 +13,15 @@ $grayL1: #e0e0e0;
$grayL2: #eeeeee;
$grayL3: #f5f5f5;
$grayL4: #fafafa;
$orange: #ff7043;
$paleOrange: #fbe9e7;
$orange400: #ff7043;
$orange500: #ff5722;
// Global SCSS styling
// @see https://docs.nativescript.org/ui/styling
// * {
// font-size: 16;
// }
Page {
font-family: "Orkney-Regular";
}
@ -32,13 +34,19 @@ Page {
.bx {
font-family: "boxicons";
font-size: 24;
vertical-alignment: center;
&.small {
padding: 0;
font-size: 16;
}
}
.ns-light {
Page,
ActionBar,
SearchBar,
TabView {
TabView,
ListPicker {
color: $grayD4;
background: $grayL4;
}
@ -53,49 +61,57 @@ Page {
.fieldLabel {
background: $grayL4;
}
.view-reference-text,
.view-reference-container {
.referenceItem,
.recipeText,
.overviewItem,
.recipeItem {
background: white;
}
.option-highlight {
.option-highlight,
.selected-sd-item {
background: $grayL2;
}
.sd-item,
.sd-group-header,
.recipe-time {
.time .bx {
color: $grayD2;
}
.fab-button {
color: white;
background-color: $orange500;
}
.option,
.icon-option {
.bx,
.option-info {
color: $grayD2;
}
.option-title {
color: $grayD4;
}
}
.view-imageHolder,
.recipeImgContainer {
.imageHolder {
color: $grayL1;
background: $grayL2;
}
.view-other {
color: $grayD2;
}
.view-count {
.count {
color: $grayL4;
background: $grayD4;
}
.view-instruction {
.instruction {
border-color: $grayD4;
}
.text-btn,
.group-header,
.category,
ActivityIndicator,
.selected-sd-item {
color: $orange500;
}
}
.ns-dark {
Page,
ActionBar,
SearchBar,
TabView {
TabView,
ListPicker {
color: $grayL4;
background: $grayD4;
}
@ -106,50 +122,57 @@ Page {
.hr {
border-color: #111;
}
.sd,
.fieldLabel {
background: $grayD4;
}
.recipe-li,
.referenceItem,
.recipeText,
.overviewItem,
.recipeItem,
.option-highlight,
.view-reference-text,
.view-reference-container {
.appIconContainer {
background: $grayD3;
}
.sd-item,
.sd-group-header,
.recipe-time {
.time .bx {
color: $grayL2;
}
.selected-sd-item {
background: #111;
}
.fab-button {
color: #111;
background: $orange400;
}
.option,
.icon-option {
.bx,
.option-info {
color: $grayL2;
}
.option-title {
color: $grayL4;
}
}
.view-imageHolder,
.recipeImgContainer {
.imageHolder {
color: $grayD4;
background: #111;
}
.view-other {
color: $grayL2;
}
.view-count {
.count {
color: $grayD4;
background: $grayL4;
}
.view-instruction {
.instruction {
border-color: $grayL4;
}
.text-btn,
.group-header,
.category,
ActivityIndicator,
.selected-sd-item {
color: $orange400;
}
}
// -----------------------------
// Elements
TextField,
TextView,
@ -173,131 +196,118 @@ TabView {
margin-top: 16;
}
.fieldLabel {
font-size: 13;
font-size: 12;
margin-left: 8;
padding: 0 8;
}
// DateTimePicker
.date-time-picker,
.date-time-picker-spinners {
color: $grayD4;
background: $grayL4;
&.ns-dark {
color: $grayL4;
background: $grayD4;
}
}
.ns-dark .date-time-picker-spinners {
color: $grayL4;
background: $grayD4;
}
// -----------------------------
// ActionBar
ActionBar {
width: 100%;
margin: 24 0 0 0;
padding: 0;
height: 64;
.bx {
padding: 16 12;
vertical-alignment: center;
}
.leftAction {
padding: 16 16 16 4;
margin: 0;
vertical-alignment: center;
}
.title {
padding-left: 8;
text-align: left;
font-size: 18;
}
}
.actionBarContainer {
margin: 0;
padding: 0;
}
SearchBar {
width: 100%;
font-size: 16;
}
.title {
padding-left: 8;
text-align: left;
font-size: 18;
}
// -----------------------------
// Side Drawer
.sd {
margin-top: 24;
padding: 32 8 8;
}
.sd-item {
border-radius: 4;
padding: 0 16;
height: 48;
font-size: 16;
vertical-alignment: center;
// prettier-ignore
.bx, Label {
vertical-alignment: center;
.bx {
font-size: 24;
}
&.selected-sd-item {
background: $paleOrange;
color: $orange;
// prettier-ignore
Label {
font-size: 16;
vertical-alignment: center;
}
}
.sd-group-header {
width: 100%;
padding: 8 8 16;
font-size: 12;
}
// Home
RadListView {
margin: 8 0 128;
}
.recipe-li {
background: white;
margin: 8 16;
border-radius: 6;
.recipe-info {
margin: 4;
}
.recipe-cat {
font-size: 12;
padding: 0;
margin: 0;
}
.recipe-title {
font-size: 16;
line-height: 4;
margin: 0;
padding: 4 0;
}
.recipe-time {
padding: 0;
}
.recipe-cat {
color: $orange;
}
}
.emptyState {
// -----------------------------
// HOME
.emptyState,
.noResult {
width: 100%;
height: 100%;
}
.noResults {
width: 100%;
height: 100%;
font-size: 16;
.noResult {
line-height: 8;
padding: 64 16;
text-align: center;
font-size: 16;
horizontal-alignment: center;
// vertical-alignment: center;
.title {
font-size: 20;
text-align: center;
}
.bx {
vertical-alignment: center;
.icon {
font-size: 64;
text-align: center;
color: $gray;
margin-bottom: 16;
}
.title {
font-size: 20;
text-align: center;
padding: 0;
margin: 0;
horizontal-alignment: center;
.bx {
font-size: 24;
vertical-alignment: center;
}
}
}
// -----------------------------
// Recipe Items
RadListView {
margin: 0 0 128;
font-size: 16;
}
.recipeItem {
margin: 8 16;
border-radius: 6;
.recipeInfo {
margin: 4;
.category,
.time {
font-size: 12;
padding: 0;
margin: 0;
}
.title {
margin: 0;
padding: 4 0;
}
.timeContainer {
padding: 0;
.time {
padding: 0 0 0 4;
}
}
}
}
.imageHolder {
vertical-alignment: center;
border-radius: 6 0 0 6;
// prettier-ignore
Image {
border-radius: 6 0 0 6;
}
}
.swipe-item {
margin: 0 8;
@ -306,167 +316,166 @@ RadListView {
height: 128;
border-radius: 6;
}
.recipeImgContainer {
vertical-alignment: center;
// prettier-ignore
Image {
border-radius: 6 0 0 6;
}
}
// Settings
// -----------------------------
// SETTINGS
.group-header {
padding: 8;
color: #ff7043;
}
.main-container {
padding: 16 8 128;
.option {
padding: 16;
border-radius: 4;
font-size: 16;
padding: 16;
.bx {
margin: 0 24 0 0;
}
.option-info {
font-size: 12;
}
}
}
// About
.app-icon-container {
margin: 32 0;
// -----------------------------
// ABOUT
.appIconContainer {
padding: 32 0;
width: 100%;
.app-icon {
.appIcon {
width: 56;
height: 56;
margin: 0 6 0 0;
padding: 0;
}
.app-name {
.appName {
font-size: 24;
}
}
.icon-option {
padding: 16;
border-radius: 4;
.bx {
margin: 0 24 0 0;
}
.option-title {
// -----------------------------
// VIEW RECIPE
.viewRecipe {
.category,
.time,
.ingredient,
.ingredient-check {
font-size: 16;
}
}
// View Recipe
.view-cat {
font-size: 16;
color: #ff7043;
}
.view-other {
font-size: 16;
}
.view-title {
font-size: 22;
line-height: 6;
margin-bottom: 16;
}
.view-ingredient {
font-size: 16;
line-height: 6;
padding-bottom: 12;
}
.activity-indicator {
background: #ff7043;
}
.view-count {
font-size: 12;
width: 24;
height: 24;
padding-top: 4%;
margin: 0 0 0 8;
text-align: center;
border-radius: 100;
&.note {
clip-path: polygon(
5% 0,
95% 0,
100% 5%,
100% 65%,
65% 100%,
5% 100%,
0 95%,
0 5%
);
.category {
margin: 0 8;
}
.time {
margin: 0 8;
.bx {
vertical-align: top;
}
}
.title {
font-size: 22;
line-height: 6;
}
.overviewContainer {
margin-top: 12;
.overviewItem {
border-radius: 6;
padding: 8;
margin: 8;
android-elevation: 1;
.bx {
color: $gray;
width: 24;
horizontal-alignment: left;
}
.itemCount {
font-size: 16;
padding: 8 4 4;
}
}
}
.ingredient,
.ingredient-check {
line-height: 6;
padding-bottom: 12;
}
.ingredient-check {
margin-bottom: 12;
}
.count {
width: 24;
height: 24;
padding-top: 4%;
margin: 0 0 0 8;
text-align: center;
border-radius: 100;
&.square {
clip-path: polygon(
5% 0,
95% 0,
100% 5%,
100% 65%,
65% 100%,
5% 100%,
0 95%,
0 5%
);
}
}
.instruction,
.note,
.reference {
font-size: 16;
line-height: 6;
padding: 2 0 14 36;
margin: 0 0 0 19;
border-width: 0 0 0 2;
}
.instruction.noBorder {
border-color: transparent;
}
.note {
border-width: 0;
}
.referenceItem {
padding: 14 16;
margin: 8 16 8;
border-radius: 6;
font-size: 16;
.bx {
font-size: 24;
}
.recipeLink {
padding: 0 16 0 0;
margin: 0;
}
}
.recipeText {
font-size: 16;
line-height: 6;
padding: 16;
margin: 8 16 8;
border-radius: 6;
}
}
.view-instruction,
.view-note,
.view-reference {
font-size: 16;
line-height: 6;
padding: 2 0 14 36;
margin: 0 0 0 19;
border-width: 0 0 0 2;
}
.view-instruction.instructionWOBorder {
border-color: transparent;
}
.view-note {
border-width: 0;
}
.view-reference-container {
padding: 14 16;
margin: 8 16 8;
border-radius: 6;
}
.view-reference {
padding: 0 16 0 0;
margin: 0;
border-width: 0;
}
.view-reference-text {
font-size: 16;
line-height: 6;
padding: 16;
margin: 8 16 8;
border-radius: 6;
}
// .view-copyReference {
// color: #ff7043;
// }
// -----------------------------
// FAB
#btnFabContainer {
width: 100%;
height: 100%;
}
// Edit Recipe
.fab-button {
color: #fff;
height: 56;
width: 56;
background-color: #ff7043;
vertical-alignment: center;
// horizontal-alignment: center;
border-radius: 28;
padding: 16;
margin: 16;
vertical-alignment: center;
android-elevation: 6;
&.negative {
background-color: #e53935;
}
}
// -----------------------------
// EDIT RECIPE
.sectionTitle {
font-size: 20;
}
.sec-btn {
.text-btn {
font-size: 14;
color: #ff7043;
text-align: left;
horizontal-alignment: left;
padding: 16;
margin: 8 0 0 0;
}
@ -474,14 +483,13 @@ RadListView {
padding: 4;
margin-top: 16;
}
// Dialogs
// -----------------------------
// DIALOGS
.dialogContainer {
width: 100%;
&.light {
color: $grayD4;
background: $grayL4;
}
color: $grayD4;
background: $grayL4;
font-size: 16;
&.dark {
color: $grayL4;
background: $grayD4;
@ -490,60 +498,62 @@ RadListView {
padding: 24 24 12;
font-size: 20;
}
.dialogInputField {
.dialogInput {
padding: 0 24 16;
}
.dialogDescription {
font-size: 16;
line-height: 4;
padding: 0 24 16;
}
.action {
padding: 24 24 24 8;
font-size: 12;
color: #ff7043;
}
.actionItem {
width: 100%;
font-size: 16;
padding: 8 20;
padding: 8 24;
margin: 0;
}
.cancel {
.actionsContainer {
padding: 24;
}
.action {
font-size: 12;
color: #ff7043;
}
}
ActivityIndicator {
color: #ff7043;
padding: 16 12;
margin: 16 10 16 8;
}
// Vue Transitions
// -----------------------------
ActivityIndicator {
width: 24;
height: 24;
margin: 16;
}
// -----------------------------
// Transitions
.bounce-enter-active {
animation-name: bounce-in;
animation-duration: 0.5s;
animation-duration: 1s;
animation-fill-mode: forwards;
animation-timing-function: ease-out;
animation-timing-function: ease-in-out;
}
.bounce-leave-active {
animation-name: bounce-in;
animation-duration: 0.1s;
animation-fill-mode: forwards;
animation-direction: reverse;
animation-timing-function: ease-in;
animation-timing-function: ease;
}
@keyframes bounce-in {
0% {
transform: scale(0);
opacity: 0;
}
50% {
25% {
transform: scale(1.2);
opacity: 1;
}
50% {
transform: scale(0.9);
}
75% {
transform: scale(1.1);
}
100% {
transform: scale(1);
@ -554,7 +564,7 @@ ActivityIndicator {
animation-duration: 1s;
animation-delay: 0.25s;
animation-fill-mode: forwards;
animation-timing-function: ease;
animation-timing-function: ease-in-out;
}
.dolly-leave-active {
opacity: 0;

File diff suppressed because it is too large Load diff

View file

@ -1,86 +1,81 @@
<template>
<Page @loaded="initializePage">
<ActionBar :flat="viewIsScrolled ? false : true">
<!-- Settings Actionbar -->
<GridLayout rows="*" columns="auto, *" class="actionBarContainer">
<GridLayout rows="*" columns="auto, *">
<Label
class="bx leftAction"
class="bx"
:text="icon.menu"
automationText="Menu"
automationText="Back"
@tap="showDrawer"
col="0"
/>
<Label class="title orkm" text="About" col="1" />
</GridLayout>
</ActionBar>
<ScrollView scrollBarIndicatorVisible="false">
<ScrollView scrollBarIndicatorVisible="false" @scroll="onScroll">
<StackLayout class="main-container">
<StackLayout
horizontalAlignment="center"
orientation="horizontal"
class="app-icon-container"
class="appIconContainer"
>
<Image src="res://icon" class="app-icon" stretch="fill" />
<Image src="res://icon" class="appIcon" stretch="fill" />
<Label
text="EnRecipes"
verticalAlignment="center"
class="app-name orkb"
class="appName orkb"
/>
</StackLayout>
<StackLayout orientation="horizontal" class="icon-option">
<Label verticalAlignment="center" class="bx" :text="icon.info" />
<StackLayout orientation="horizontal" class="option">
<Label class="bx" :text="icon.info" />
<StackLayout>
<Label text="Version" class="option-title" />
<Label text="1.0.0" class="option-info" textWrap="true" />
<Label text="Version" />
<Label :text="getVersion" class="option-info" textWrap="true" />
</StackLayout>
</StackLayout>
<StackLayout
orientation="horizontal"
class="icon-option"
class="option"
@tap="openURL($event, 'https://github.com/vishnuraghavb/enrecipes')"
>
<Label class="bx" :text="icon.link" />
<Label text="View project on GitHub" class="option-title" />
<Label text="View project on GitHub" />
</StackLayout>
<StackLayout
orientation="horizontal"
class="icon-option"
class="option"
@tap="openURL($event, 'https://t.me/enrecipes')"
>
<Label class="bx" :text="icon.telegram" />
<Label text="Join the telegram group" class="option-title" />
<Label text="Join the Telegram group" />
</StackLayout>
<!-- <StackLayout orientation="horizontal" class="icon-option">
<Label class="bx" :text="icon.file" />
<Label text="Licenses" class="option-title" />
</StackLayout> -->
<StackLayout class="hr m-10"></StackLayout>
<Label text="Author" class="group-header" />
<StackLayout
orientation="horizontal"
class="icon-option"
class="option"
@tap="openURL($event, 'https://www.vishnuraghav.com')"
>
<Label class="bx" :text="icon.user" />
<Label text="Vishnu Raghav" class="option-title" />
<Label text="Vishnu Raghav" />
</StackLayout>
<StackLayout
orientation="horizontal"
class="icon-option"
class="option"
@tap="openURL($event, 'https://github.com/vishnuraghavb')"
>
<Label class="bx" :text="icon.link" />
<Label text="Follow on GitHub" class="option-title" />
<Label text="Follow on GitHub" />
</StackLayout>
<StackLayout
orientation="horizontal"
class="icon-option"
class="option"
@tap="openURL($event, 'https://mastodon.social/@vishnuraghavb')"
>
<Label class="bx" :text="icon.link" />
<Label text="Follow on Mastodon" class="option-title" />
<Label text="Follow on Mastodon" />
</StackLayout>
</StackLayout>
</ScrollView>
@ -88,18 +83,33 @@
</template>
<script>
import { Utils } from "@nativescript/core"
import { Utils, Application } from "@nativescript/core"
import { mapState, mapActions } from "vuex"
export default {
props: ["highlight", "viewIsScrolled", "showDrawer", "title"],
props: ["highlight", "showDrawer", "title"],
computed: {
...mapState(["icon", "currentComponent"]),
getVersion() {
let ctx = Application.android.context
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0)
.versionName
},
},
data() {
return {
viewIsScrolled: false,
}
},
methods: {
...mapActions(["setCurrentComponentAction"]),
initializePage() {
this.setCurrentComponentAction("About")
},
onScroll(args) {
args.scrollY
? (this.viewIsScrolled = true)
: (this.viewIsScrolled = false)
},
openURL(args, url) {
this.highlight(args)
Utils.openUrl(url)

View file

@ -8,27 +8,22 @@
gesturesEnabled="true"
drawerTransition="SlideInOnTopTransition"
>
<GridLayout
rows="*, auto"
columns="*"
~drawerContent
padding="8"
class="sd"
>
<GridLayout rows="*, auto" columns="*" ~drawerContent class="sd">
<StackLayout row="0">
<StackLayout
<GridLayout
rows="48"
columns="auto, 24, *"
v-for="(item, index) in topmenu"
:key="index"
@tap="navigateTo(item.component, false, false)"
orientation="horizontal"
class="sd-item orkm"
:class="{
'selected-sd-item': currentComponent === item.component,
}"
>
<Label class="bx" :text="icon[item.icon]" margin="0 24 0 0" />
<Label :text="item.title" />
</StackLayout>
<Label col="0" row="0" class="bx" :text="icon[item.icon]" />
<Label col="2" row="0" :text="item.title" />
</GridLayout>
<StackLayout class="hr m-10"></StackLayout>
<GridLayout
class="sd-group-header orkm"
@ -64,7 +59,7 @@
<Label col="1" :text="item" />
<Label
v-if="catEditMode"
@tap="editCategory(item)"
@tap="renameCategory(item)"
col="2"
class="bx"
:text="icon.edit"
@ -100,7 +95,7 @@
</GridLayout>
<GridLayout ~mainContent rows="*" columns="*">
<Frame ref="mainFrame" id="main-frame">
<Frame row="0" col="0" ref="mainFrame" id="main-frame">
<!-- Home -->
<EnRecipes
ref="enrecipes"
@ -179,7 +174,13 @@ export default {
}
},
computed: {
...mapState(["icon", "recipes", "categories", "currentComponent"]),
...mapState([
"icon",
"recipes",
"categories",
"yieldUnits",
"currentComponent",
]),
categoriesWithRecipes() {
let arr = this.recipes.map((e) => {
return e.category
@ -192,11 +193,12 @@ export default {
"setCurrentComponentAction",
"initializeRecipes",
"initializeCategories",
"initializeYieldUnits",
"renameCategoryAction",
]),
toggleCatEdit() {
this.catEditMode = !this.catEditMode
this.setComponent("EnRecipes")
if (this.selectedCategory) this.setComponent("EnRecipes")
this.filterFavorites = this.filterTrylater = false
this.selectedCategory = null
this.$refs.enrecipes.updateFilter()
@ -204,7 +206,7 @@ export default {
setComponent(comp) {
this.setCurrentComponentAction(comp)
},
editCategory(category) {
renameCategory(category) {
this.releaseGlobalBackEvent()
this.$showModal(PromptDialog, {
props: {
@ -217,6 +219,7 @@ export default {
if (newCategory.length) {
this.renameCategoryAction({ current: category, updated: newCategory })
this.catEditMode = false
this.navigateTo(newCategory, false, true)
}
})
},
@ -279,7 +282,6 @@ export default {
frame: "main-frame",
props: {
highlight: this.highlight,
viewIsScrolled: this.viewIsScrolled,
showDrawer: this.showDrawer,
restartApp: this.restartApp,
hijackGlobalBackEvent: this.hijackGlobalBackEvent,
@ -340,6 +342,7 @@ export default {
setTimeout((e) => Theme.setMode(Theme[themeName]), 50)
if (!this.recipes.length) this.initializeRecipes()
if (!this.categories.length) this.initializeCategories()
if (!this.yieldUnits.length) this.initializeYieldUnits()
},
}
</script>

View file

@ -1,9 +1,9 @@
<template>
<Page @loaded="initialize" @unloaded="releaseBackEvent">
<ActionBar :flat="viewIsScrolled ? false : true">
<GridLayout rows="*" columns="auto, *, auto" class="actionBarContainer">
<GridLayout rows="*" columns="auto, *, auto">
<Label
class="bx leftAction"
class="bx"
:text="icon.back"
automationText="Back"
col="0"
@ -11,19 +11,20 @@
/>
<Label class="title orkm" :text="title" col="1" />
<Label
v-if="hasEnoughDetails"
v-if="hasEnoughDetails && !imageLoading"
class="bx"
:text="icon.save"
col="2"
@tap="saveRecipe"
@tap="saveOperation"
/>
<ActivityIndicator col="2" v-if="imageLoading" :busy="imageLoading" />
</GridLayout>
</ActionBar>
<AbsoluteLayout>
<GridLayout rows="*" columns="*">
<ScrollView
width="100%"
height="100%"
@scroll="onScroll($event)"
row="0"
col="0"
@scroll="onScroll"
scrollBarIndicatorVisible="false"
>
<StackLayout width="100%" padding="0 0 128">
@ -32,7 +33,7 @@
<StackLayout
width="100%"
:height="screenWidth"
class="view-imageHolder"
class="imageHolder"
verticalAlignment="center"
>
<Image
@ -47,7 +48,7 @@
horizontalAlignment="center"
class="bx"
fontSize="160"
:text="icon.image"
:text="icon.food"
/>
</StackLayout>
<StackLayout width="100%" :top="screenWidth - 42">
@ -55,7 +56,7 @@
<Label
v-if="showFab"
horizontalAlignment="right"
@tap="photoHandler"
@tap="imageHandler"
class="bx fab-button"
:text="icon.camera"
androidElevation="6"
@ -76,43 +77,41 @@
</AbsoluteLayout>
<AbsoluteLayout class="inputField">
<TextField
v-model="recipeContent.category"
:text="recipeContent.category"
editable="false"
@tap="showCategories()"
@tap="showCategories"
/>
<Label top="0" class="fieldLabel" text="Category" />
</AbsoluteLayout>
<GridLayout columns="*, 8, *">
<AbsoluteLayout class="inputField" col="0">
<TimePickerField
titleTextColor="red"
timeFormat="HH:mm"
pickerTitle="Approx. preparation time"
@timeChange="onPrepTimeChange"
:time="recipeContent.prepTime"
></TimePickerField>
<Label top="0" class="fieldLabel" text="Preparation time" />
<TextField
v-model="recipeContent.yield.quantity"
hint="1"
keyboardType="number"
/>
<Label top="0" class="fieldLabel" text="Yield quantity" />
</AbsoluteLayout>
<AbsoluteLayout class="inputField" col="2">
<TimePickerField
timeFormat="HH:mm"
pickerTitle="Approx. cooking time"
@timeChange="onCookTimeChange"
:time="recipeContent.cookTime"
></TimePickerField>
<Label top="0" class="fieldLabel" text="Cooking time" />
<TextField
:text="recipeContent.yield.unit"
editable="false"
@tap="showYieldUnits"
/>
<Label top="0" class="fieldLabel" text="Yield measured in" />
</AbsoluteLayout>
</GridLayout>
<GridLayout columns="*, 16, *">
<GridLayout columns="*, 8, *">
<AbsoluteLayout class="inputField" col="0">
<TextField
width="100%"
keyboardType="number"
v-model="recipeContent.portionSize"
:text="formattedTimeRequired"
editable="false"
@tap="setTimeRequired"
/>
<Label top="0" class="fieldLabel" text="Portion size" />
<Label top="0" class="fieldLabel" text="Time required" />
</AbsoluteLayout>
</GridLayout>
<StackLayout class="hr" margin="24 16"></StackLayout>
</StackLayout>
@ -125,6 +124,7 @@
:key="index"
>
<TextField
@loaded="focusField"
col="0"
v-model="recipeContent.ingredients[index].item"
:hint="`Item ${index + 1}`"
@ -153,7 +153,7 @@
/>
</GridLayout>
<Label
class="sec-btn pull-left orkm"
class="text-btn orkm"
text="+ ADD INGREDIENT"
@tap="addIngredient()"
/>
@ -170,6 +170,7 @@
:key="index"
>
<TextView
@loaded="focusField"
col="0"
:hint="`Step ${index + 1}`"
v-model="recipeContent.instructions[index]"
@ -183,7 +184,7 @@
/>
</GridLayout>
<Label
class="sec-btn pull-left orkm"
class="text-btn orkm"
text="+ ADD STEP"
@tap="addInstruction()"
/>
@ -199,6 +200,7 @@
:key="index"
>
<TextView
@loaded="focusField"
col="0"
v-model="recipeContent.notes[index]"
:hint="`Note ${index + 1}`"
@ -211,11 +213,7 @@
@tap="removeNote(index)"
/>
</GridLayout>
<Label
class="sec-btn pull-left orkm"
text="+ ADD NOTE"
@tap="addNote()"
/>
<Label class="text-btn orkm" text="+ ADD NOTE" @tap="addNote()" />
<StackLayout class="hr" margin="24 16"></StackLayout>
</StackLayout>
@ -228,6 +226,7 @@
:key="index"
>
<TextView
@loaded="focusField"
col="0"
v-model="recipeContent.references[index]"
hint="Text or Website/Video URL"
@ -241,7 +240,7 @@
/>
</GridLayout>
<Label
class="sec-btn pull-left orkm"
class="text-btn orkm"
text="+ ADD REFERENCE"
@tap="addReference()"
/>
@ -249,11 +248,14 @@
</StackLayout>
</StackLayout>
</ScrollView>
</AbsoluteLayout>
</GridLayout>
</Page>
</template>
<script>
import { WorkerService } from "../worker.service"
var workerService = new WorkerService()
import {
Screen,
AndroidApplication,
@ -261,14 +263,20 @@ import {
path,
getFileAccess,
knownFolders,
Utils,
File,
} from "@nativescript/core"
import { Mediafilepicker } from "nativescript-mediafilepicker"
import { DateTimePicker, TimePickerField } from "@nativescript/datetimepicker"
import { mapState, mapActions } from "vuex"
import ActionDialog from "./modal/ActionDialog.vue"
import ConfirmDialog from "./modal/ConfirmDialog.vue"
import PromptDialog from "./modal/PromptDialog.vue"
import ListPicker from "./modal/ListPicker.vue"
import { create } from "domain"
export default {
props: ["recipeIndex", "recipeID", "selectedCategory"],
@ -280,31 +288,36 @@ export default {
imageSrc: null,
title: undefined,
category: "Undefined",
prepTime: "00:00",
cookTime: "00:00",
portionSize: 1,
ingredients: [
{
item: "",
quantity: undefined,
unit: "unit",
},
],
instructions: [""],
notes: [""],
references: [""],
timeRequired: "00:00",
yield: {
quantity: undefined,
unit: "Servings",
},
ingredients: [],
instructions: [],
notes: [],
references: [],
isFavorite: false,
tried: false,
lastTried: null,
lastModified: null,
},
tempRecipeContent: {},
blockModal: false,
newRecipeID: null,
showFab: false,
imageLoading: false,
}
},
computed: {
...mapState(["icon", "units", "recipes", "categories", "currentComponent"]),
...mapState([
"icon",
"units",
"yieldUnits",
"recipes",
"categories",
"currentComponent",
]),
screenWidth() {
return Screen.mainScreen.widthDIPs
},
@ -314,6 +327,12 @@ export default {
JSON.stringify(this.tempRecipeContent)
)
},
formattedTimeRequired() {
let t = this.recipeContent.timeRequired.split(":")
let h = parseInt(t[0])
let m = parseInt(t[1])
return h ? (m ? `${h}h ${m}m` : `${h}h`) : `${m}m`
},
},
methods: {
...mapActions([
@ -321,10 +340,19 @@ export default {
"addRecipeAction",
"overwriteRecipeAction",
"addCategoryAction",
"addYieldUnitAction",
]),
initialize() {
this.showFab = true
},
// HELPERS
focusField(args) {
if (!args.object.text) {
args.object.focus()
setTimeout((e) => Utils.ad.showSoftInput(args.object.android), 1)
}
},
getRandomID() {
let res = ""
let chars = "abcdefghijklmnopqrstuvwxyz0123456789"
@ -333,87 +361,41 @@ export default {
}
return res
},
setTime(key, time) {
if (Date.parse(time)) {
let date = new Date(time)
let h = date.getHours()
let m = date.getMinutes()
this.recipeContent[key] =
(h < 10 ? "0" + h : h) + ":" + (m < 10 ? "0" + m : m)
}
},
clearEmptyFields() {
if (!this.recipeContent.title) {
this.recipeContent.title = "Untitled Recipe"
}
if (!this.recipeContent.portionSize) {
this.recipeContent.portionSize = 1
}
this.recipeContent.ingredients.forEach((e, i) => {
if (!e.item.length) {
this.recipeContent.ingredients.splice(i, 1)
setTimeRequired() {
let time = this.recipeContent.timeRequired.split(":")
let hr = time[0]
let min = time[1]
this.$showModal(ListPicker, {
props: {
title: "Approx. time required",
action: "SET",
selectedHr: hr,
selectedMin: min,
},
}).then((result) => {
if (result) {
this.recipeContent.timeRequired = result
}
})
let vm = this
function removeEmpty(arr) {
vm.recipeContent[arr].forEach((e, i) => {
if (!e.length) {
vm.recipeContent[arr].splice(i, 1)
}
})
}
removeEmpty("instructions")
removeEmpty("notes")
removeEmpty("references")
},
saveRecipe() {
console.log(
JSON.stringify(this.recipeContent),
JSON.stringify(this.tempRecipeContent)
)
this.clearEmptyFields()
this.recipeContent.lastModified = new Date()
if (this.recipeID) {
this.overwriteRecipeAction({
index: this.recipeIndex,
id: this.recipeID,
recipe: this.recipeContent,
})
} else {
this.recipeContent.id = this.newRecipeID
this.addRecipeAction({
id: this.newRecipeID,
recipe: this.recipeContent,
})
}
if (this.tempRecipeContent.imageSrc && !this.recipeContent.imageSrc) {
getFileAccess().deleteFile(this.tempRecipeContent.imageSrc)
}
this.$navigateBack()
},
onPrepTimeChange(args) {
this.setTime("prepTime", args.value)
},
onCookTimeChange(args) {
this.setTime("cookTime", args.value)
},
onScroll(args) {
args.scrollY
? (this.viewIsScrolled = true)
: (this.viewIsScrolled = false)
},
// DATA LIST
showCategories() {
this.releaseBackEvent()
this.$showModal(ActionDialog, {
props: {
title: "Category",
list: [...this.categories],
height: "60%",
action: "NEW CATEGORY",
height: "408",
action: "CREATE NEW",
},
}).then((action) => {
if (action == "NEW CATEGORY") {
if (action == "CREATE NEW") {
this.$showModal(PromptDialog, {
props: {
title: "New category",
@ -434,6 +416,52 @@ export default {
}
})
},
showYieldUnits() {
this.releaseBackEvent()
this.$showModal(ActionDialog, {
props: {
title: "Yield measured in",
list: [...this.yieldUnits],
height: "408",
action: "CREATE NEW",
},
}).then((action) => {
if (action == "CREATE NEW") {
this.$showModal(PromptDialog, {
props: {
title: "New yield unit",
action: "ADD",
},
}).then((yieldUnit) => {
this.hijackBackEvent()
if (yieldUnit.length) {
this.recipeContent.yield.unit = yieldUnit
this.addYieldUnitAction(yieldUnit)
}
})
} else if (action) {
this.recipeContent.yield.unit = action
this.hijackBackEvent()
} else {
this.hijackBackEvent()
}
})
},
showUnits(e) {
this.releaseBackEvent()
this.$showModal(ActionDialog, {
props: {
title: "Unit",
list: [...this.units],
height: "408",
},
}).then((action) => {
this.hijackBackEvent()
if (action) e.object.text = action
})
},
// NAVIGATION HANDLERS
navigateBack() {
if (this.hasEnoughDetails) {
this.blockModal = true
@ -448,7 +476,7 @@ export default {
}).then((action) => {
this.blockModal = false
if (action) {
this.saveRecipe()
this.saveOperation()
} else if (action != null) {
this.$navigateBack()
this.releaseBackEvent()
@ -477,7 +505,9 @@ export default {
this.navigateBack()
}
},
photoHandler() {
// DATA HANDLERS
imageHandler() {
if (this.recipeContent.imageSrc) {
this.blockModal = true
this.$showModal(ConfirmDialog, {
@ -489,17 +519,17 @@ export default {
}).then((action) => {
this.blockModal = false
if (action) {
this.takePicture()
this.imagePicker()
} else if (action != null) {
this.removePicture()
this.recipeContent.imageSrc = null
this.releaseBackEvent()
}
})
} else {
this.takePicture()
this.imagePicker()
}
},
takePicture() {
imagePicker() {
const vm = this
const mediafilepicker = new Mediafilepicker()
mediafilepicker.openImagePicker({
@ -511,36 +541,7 @@ export default {
},
})
mediafilepicker.on("getFiles", (image) => {
let result = image.object.get("results")[0].file
ImageSource.fromFile(result).then((savedImg) => {
let savedImgPath = path.join(
knownFolders.documents().getFolder("enrecipes").path,
`${vm.getRandomID()}.jpg`
)
savedImg.saveToFile(savedImgPath, "jpg")
vm.recipeContent.imageSrc = savedImgPath
})
})
mediafilepicker.on("error", function(res) {
let msg = res.object.get("msg")
console.log(msg)
})
mediafilepicker.on("cancel", function(res) {
let msg = res.object.get("msg")
console.log(msg)
})
},
removePicture() {
confirm({
title: "Delete Photo",
message: "Are you sure you want to delete the recipe photo?",
okButtonText: "Delete",
cancelButtonText: "Cancel",
}).then((e) => {
if (e) {
this.recipeContent.imageSrc = null
}
vm.recipeContent.imageSrc = image.object.get("results")[0].file
})
},
@ -576,18 +577,70 @@ export default {
this.recipeContent.references.splice(index, 1)
},
showUnits(e) {
this.releaseBackEvent()
this.$showModal(ActionDialog, {
props: {
title: "Unit",
list: [...this.units],
height: "75%",
},
}).then((action) => {
this.hijackBackEvent()
if (action) e.object.text = action
clearEmptyFields() {
if (!this.recipeContent.title)
this.recipeContent.title = "Untitled Recipe"
if (!this.recipeContent.yield.quantity)
this.recipeContent.yield.quantity = 1
this.recipeContent.ingredients = this.recipeContent.ingredients.filter(
(e) => e.item
)
let vm = this
function clearEmpty(arr) {
vm.recipeContent[arr] = vm.recipeContent[arr].filter((e) => e)
}
clearEmpty("instructions")
clearEmpty("notes")
clearEmpty("references")
},
saveOperation() {
this.imageLoading = true
this.clearEmptyFields()
this.recipeContent.lastModified = new Date()
if (this.recipeContent.imageSrc) {
if (this.tempRecipeContent.imageSrc) {
if (this.tempRecipeContent.imageSrc !== this.recipeContent.imageSrc) {
getFileAccess().deleteFile(this.tempRecipeContent.imageSrc)
this.imageSaveOperation()
} else this.saveRecipe()
} else this.imageSaveOperation()
} else if (this.tempRecipeContent.imageSrc) {
getFileAccess().deleteFile(this.tempRecipeContent.imageSrc)
this.saveRecipe()
} else this.saveRecipe()
},
imageSaveOperation() {
let imgSavedToPath = path.join(
knownFolders.documents().getFolder("enrecipes").path,
`${this.getRandomID()}.jpg`
)
let workerService = new WorkerService()
let ImageProcessor = workerService.initImageProcessor()
ImageProcessor.postMessage({
imgFile: this.recipeContent.imageSrc,
imgSavedToPath,
})
ImageProcessor.onmessage = ({ data }) => {
this.recipeContent.imageSrc = imgSavedToPath
this.saveRecipe()
}
},
saveRecipe() {
if (this.recipeID) {
this.overwriteRecipeAction({
index: this.recipeIndex,
id: this.recipeID,
recipe: this.recipeContent,
})
} else {
this.recipeContent.id = this.newRecipeID
this.addRecipeAction({
id: this.newRecipeID,
recipe: this.recipeContent,
})
}
this.imageLoading = false
this.$navigateBack()
},
},
created() {
@ -598,7 +651,10 @@ export default {
if (this.recipeID) {
let recipe = this.recipes.filter((e) => e.id === this.recipeID)[0]
Object.assign(this.recipeContent, JSON.parse(JSON.stringify(recipe)))
Object.assign(this.tempRecipeContent, JSON.parse(JSON.stringify(recipe)))
Object.assign(
this.tempRecipeContent,
JSON.parse(JSON.stringify(this.recipeContent))
)
} else {
Object.assign(
this.tempRecipeContent,

View file

@ -5,11 +5,10 @@
<GridLayout
v-if="showSearch"
columns="auto, *"
class="actionBarContainer"
verticalAlignment="center"
>
<Label
class="bx leftAction"
class="bx"
:text="icon.back"
automationText="Back"
col="0"
@ -25,15 +24,11 @@
/>
</GridLayout>
<!-- Home Actionbar -->
<GridLayout
v-else
columns="auto, *, auto, auto"
class="actionBarContainer"
>
<GridLayout v-else columns="auto, *, auto, auto">
<Label
class="bx leftAction"
class="bx"
:text="icon.menu"
automationText="Menu"
automationText="Back"
@tap="showDrawer"
col="0"
/>
@ -62,19 +57,19 @@
swipeActions="true"
@itemSwipeProgressChanged="onSwiping"
@itemSwipeProgressEnded="onSwipeEnded"
@scrolled="onScroll($event)"
@scrolled="onScroll"
@itemTap="viewRecipe"
:filteringFunction="filterFunction"
:sortingFunction="sortFunction"
>
<v-template>
<GridLayout
class="recipe-li"
class="recipeItem"
rows="112"
columns="112, *"
androidElevation="1"
androidElevation="2"
>
<GridLayout class="recipeImgContainer" rows="112" columns="112">
<GridLayout class="imageHolder" rows="112" columns="112">
<Image
row="0"
col="0"
@ -89,16 +84,19 @@
horizontalAlignment="center"
class="bx"
fontSize="56"
:text="icon.image"
:text="icon.food"
/>
</GridLayout>
<StackLayout class="recipe-info" col="1">
<Label :text="recipe.category" class="orkm recipe-cat" />
<Label :text="recipe.title" class="orkm recipe-title" />
<Label
:text="recipeTotalTime(recipe.prepTime, recipe.cookTime)"
class="h4 recipe-time"
/>
<StackLayout class="recipeInfo" col="1">
<Label :text="recipe.category" class="orkm category" />
<Label :text="recipe.title" class="orkm title" />
<StackLayout class="timeContainer" orientation="horizontal">
<Label class="bx small" :text="icon.time" />
<Label
class="time"
:text="`${formattedTime(recipe.timeRequired).time}`"
/>
</StackLayout>
</StackLayout>
</GridLayout>
</v-template>
@ -117,32 +115,30 @@
<StackLayout
col="0"
row="0"
class="noResults"
class="noResult"
v-if="!recipes.length && !filterFavorites && !filterTrylater"
verticalAlignment="center"
>
<Label class="bx icon" :text="icon.plusCircle" textWrap="true" />
<Label
@tap="addRecipe"
class="bx"
:text="icon.plusCircle"
textWrap="true"
/>
<Label
class="title orkb"
class="title orkm"
text="Start adding your recipes!"
textWrap="true"
/>
<Label text='Tap the "+" icon to add a new recipe' textWrap="true" />
<StackLayout orientation="horizontal" horizontalAlignment="center">
<Label text="Use the " textWrap="true" />
<Label class="bx" :text="icon.plus" />
<Label text=" button to add a new recipe" textWrap="true" />
</StackLayout>
</StackLayout>
<StackLayout
col="0"
row="0"
class="noResults"
class="noResult"
v-if="!filteredRecipes.length && searchQuery"
verticalAlignment="top"
>
<Label class="bx" :text="icon.search" textWrap="true" />
<Label class="title orkb" text="No recipes found" textWrap="true" />
<Label class="bx icon" :text="icon.search" textWrap="true" />
<Label class="title orkm" text="No recipes found" textWrap="true" />
<Label
:text="
`Your search &quot;${searchQuery}&quot; did not match any recipes${
@ -157,12 +153,11 @@
<StackLayout
col="0"
row="0"
class="noResults"
verticalAlignment="center"
class="noResult"
v-if="!filteredRecipes.length && filterFavorites && !searchQuery"
>
<Label class="bx" :text="icon.heartOutline" textWrap="true" />
<Label class="title orkb" text="No favorites yet!" textWrap="true" />
<Label class="bx icon" :text="icon.heartOutline" textWrap="true" />
<Label class="title orkm" text="No favorites yet!" textWrap="true" />
<Label
text="Your favorited recipes will be listed here"
textWrap="true"
@ -171,19 +166,18 @@
<StackLayout
col="0"
row="0"
class="noResults"
verticalAlignment="center"
class="noResult"
v-if="!filteredRecipes.length && filterTrylater && !searchQuery"
>
<Label class="bx" :text="icon.trylaterOutline" textWrap="true" />
<Label class="bx icon" :text="icon.trylaterOutline" textWrap="true" />
<Label
class="title orkb"
text="No recipes here to try!"
class="title orkm"
text="Nothing to try next!"
textWrap="true"
/>
<!-- text="Your Try later recipes will be listed here" -->
<Label
text="Your recipes to try later will be listed here"
text="Recipes you wanted to try later will be listed here"
textWrap="true"
/>
</StackLayout>
@ -297,10 +291,10 @@ export default {
this.closeSearch()
},
closeSearch() {
if (this.searchQuery) this.updateFilter()
this.searchQuery = ""
Utils.ad.dismissSoftInput()
this.showSearch = false
this.updateFilter()
this.releaseLocalBackEvent()
},
sortDialog() {
@ -309,7 +303,7 @@ export default {
props: {
title: "Sort by",
list: ["Natural order", "Title", "Duration", "Last modified"],
height: "195", // 48*4 + 3 1dip separators
height: "216", // 54*4
},
}).then((action) => {
if (action && action !== "Cancel" && this.sortType !== action) {
@ -330,8 +324,8 @@ export default {
.localeCompare(otherItem.title.toLowerCase(), "en", {
ignorePunctuation: true,
})
let d1 = this.recipeDuration(item.prepTime, item.cookTime)
let d2 = this.recipeDuration(otherItem.prepTime, otherItem.cookTime)
let d1 = this.formattedTime(item.timeRequired).duration
let d2 = this.formattedTime(otherItem.timeRequired).duration
let ld1 = new Date(item.lastModified)
let ld2 = new Date(otherItem.lastModified)
switch (this.sortType) {
@ -411,28 +405,29 @@ export default {
this.deletionDialogActive = false
})
},
getTotalTime(prepTime, cookTime) {
let pT = prepTime.split(":")
let cT = cookTime.split(":")
let hrs = parseInt(pT[0]) + parseInt(cT[0])
let mins = parseInt(pT[1]) + parseInt(cT[1])
if (mins > 60) {
hrs += Math.floor(mins / 60)
mins -= 60
}
// getTotalTime(prepTime, timeRequired) {
// let pT = prepTime.split(":")
// let cT = timeRequired.split(":")
// let hrs = parseInt(pT[0]) + parseInt(cT[0])
// let mins = parseInt(pT[1]) + parseInt(cT[1])
// if (mins > 60) {
// hrs += Math.floor(mins / 60)
// mins -= 60
// }
// return {
// hrs,
// mins,
// }
// },
formattedTime(time) {
let t = time.split(":")
let h = parseInt(t[0])
let m = parseInt(t[1])
return {
hrs,
mins,
time: h ? (m ? `${h}h ${m}m` : `${h}h`) : `${m}m`,
duration: `${h}${m}`,
}
},
recipeTotalTime(prepTime, cookTime) {
let { hrs, mins } = this.getTotalTime(prepTime, cookTime)
return hrs ? `${hrs}h ${mins}m` : `${mins}m`
},
recipeDuration(prepTime, cookTime) {
let { hrs, mins } = this.getTotalTime(prepTime, cookTime)
return `${hrs}${mins}`
},
onScroll(args) {
args.scrollOffset
? (this.viewIsScrolled = true)
@ -448,7 +443,6 @@ export default {
// curve: "easeIn",
// },
props: {
viewIsScrolled: this.viewIsScrolled,
selectedCategory: this.selectedCategory,
},
})

View file

@ -1,56 +1,40 @@
<template>
<Page @loaded="initializePage">
<ActionBar :flat="viewIsScrolled ? false : true">
<!-- Settings Actionbar -->
<GridLayout rows="*" columns="auto, *" class="actionBarContainer">
<GridLayout rows="*" columns="auto, *">
<Label
class="bx leftAction"
class="bx"
:text="icon.menu"
automationText="Menu"
automationText="Back"
@tap="showDrawer"
col="0"
/>
<Label class="title orkm" text="Settings" col="1" />
</GridLayout>
</ActionBar>
<ScrollView scrollBarIndicatorVisible="false">
<ScrollView scrollBarIndicatorVisible="false" @scroll="onScroll">
<StackLayout class="main-container">
<Label text="Interface" class="group-header" />
<StackLayout
orientation="horizontal"
class="option"
@tap="selectThemes"
>
<!-- @tap="selectThemes" -->
<Label verticalAlignment="center" class="bx" :text="icon.theme" />
<StackLayout>
<Label text="Theme" class="option-title" />
<Label :text="themeName" class="option-info" textWrap="true" />
</StackLayout>
</StackLayout>
<StackLayout class="hr m-10"></StackLayout>
<Label text="Backup/Restore" class="group-header" />
<!-- <StackLayout
orientation="horizontal"
class="option"
@tap="selectBackupDir"
>
<Label verticalAlignment="center" class="bx" :text="icon.folder" />
<StackLayout>
<Label text="EnRecipes Backup Directory" class="option-title" />
<Label text="/storage/emulated/0/EnRecipes" class="option-info" />
</StackLayout>
</StackLayout> -->
<StackLayout orientation="horizontal" class="option" @tap="backupData">
<Label class="bx" :text="icon.backup" />
<Label text="Backup Data" class="option-title" />
<Label class="bx" :text="icon.save" />
<Label text="Backup data" />
</StackLayout>
<StackLayout orientation="horizontal" class="option" @tap="restoreData">
<Label class="bx" :text="icon.restore" />
<Label text="Restore Data" class="option-title" />
<Label text="Restore data" />
</StackLayout>
</StackLayout>
</ScrollView>
@ -78,7 +62,6 @@ import { mapState, mapActions } from "vuex"
export default {
props: [
"highlight",
"viewIsScrolled",
"showDrawer",
"restartApp",
"hijackGlobalBackEvent",
@ -86,6 +69,7 @@ export default {
],
data() {
return {
viewIsScrolled: false,
interface: {
theme: {
title: "Theme",
@ -123,9 +107,10 @@ export default {
this.setCurrentComponentAction("Settings")
this.releaseGlobalBackEvent()
},
showDialog(args) {
this.highlight(args)
this.$showModal(ActionDialog)
onScroll(args) {
args.scrollY
? (this.viewIsScrolled = true)
: (this.viewIsScrolled = false)
},
selectThemes(args) {
this.highlight(args)
@ -133,7 +118,7 @@ export default {
props: {
title: "Theme",
list: ["Light", "Dark"],
height: "97",
height: "108",
},
}).then((action) => {
if (action && action !== "Cancel" && this.themeName !== action) {
@ -177,10 +162,11 @@ export default {
android.os.Environment.DIRECTORY_DOWNLOADS
).toString()
let date = new Date()
let formattedDate = `${date.getFullYear()}${date.getMonth()}${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`
let fromPath = path.join(knownFolders.documents().path, "enrecipes")
let destPath = path.join(
sdDownloadPath,
`enrecipes_${date.toString()}.zip`
`EnRecipes-${formattedDate}.zip`
)
console.log(fromPath, destPath, sdDownloadPath)
Zip.zip({

View file

@ -1,15 +1,11 @@
<template>
<Page @loaded="initializePage">
<ActionBar height="152" margin="0" flat="true">
<GridLayout
rows="24, 64, 64"
columns="auto, *, auto,auto, auto"
class="actionBarContainer"
>
<ActionBar height="152" margin="0" flat="true" visibility="collapse">
<GridLayout rows="24, 64, 64" columns="auto, *, auto,auto, auto">
<Label
row="1"
col="0"
class="bx leftAction"
class="bx"
:text="icon.back"
automationText="Back"
@tap="$navigateBack()"
@ -50,11 +46,18 @@
:text="icon.edit"
@tap="editRecipe"
/>
<ActivityIndicator v-else row="1" col="4" :busy="busy" />
<ActivityIndicator v-else row="1" col="2" :busy="busy" />
</GridLayout>
</ActionBar>
<AbsoluteLayout>
<TabView androidElevation="0" width="100%" height="100%">
<TabView
:selectedIndex="selectedTabIndex"
@selectedIndexChange="selectedIndexChange"
androidElevation="0"
width="100%"
height="100%"
class="viewRecipe"
>
<TabViewItem title="Overview">
<ScrollView scrollBarIndicatorVisible="false">
<StackLayout>
@ -62,7 +65,7 @@
width="100%"
:height="screenWidth"
verticalAlignment="center"
class="view-imageHolder"
class="imageHolder"
>
<Image
v-if="recipe.imageSrc"
@ -76,69 +79,181 @@
horizontalAlignment="center"
class="bx"
fontSize="160"
:text="icon.image"
:text="icon.food"
/>
</StackLayout>
<StackLayout margin="16 16 144">
<Label class="view-cat orkm" :text="recipe.category" />
<StackLayout margin="16 8 144">
<Label class="category orkm" :text="recipe.category" />
<Label
class="view-title orkm"
margin="0 8"
class="title orkm"
:text="recipe.title"
textWrap="true"
/>
<Label
class="view-other"
:text="`Preparation time: ${getTime(recipe.prepTime)}`"
/>
<Label
class="view-other"
:text="`Cooking time: ${getTime(recipe.cookTime)}`"
/>
<Label class="time">
<FormattedString>
<Span text="Time required:"></Span>
<Span
:text="` ${formattedTime(recipe.timeRequired)}`"
></Span>
</FormattedString>
</Label>
<GridLayout
rows="auto, auto"
columns="*, *"
class="overviewContainer"
>
<StackLayout
class="overviewItem"
row="0"
col="0"
@tap="selectedTabIndex = 1"
>
<Label class="bx" :text="icon.item" />
<Label
class="itemCount"
:text="
`${recipe.ingredients.length} ${
recipe.ingredients.length == 1
? 'Ingredient'
: 'Ingredients'
}`
"
textWrap="true"
/>
</StackLayout>
<StackLayout
class="overviewItem"
row="0"
col="1"
@tap="selectedTabIndex = 2"
>
<Label class="bx" :text="icon.step" />
<Label
class="itemCount"
:text="
`${recipe.instructions.length} ${
recipe.instructions.length == 1 ? 'Step' : 'Steps'
}`
"
textWrap="true"
/>
</StackLayout>
<StackLayout
class="overviewItem"
row="1"
col="0"
@tap="selectedTabIndex = 3"
>
<Label class="bx" :text="icon.note" />
<Label
class="itemCount"
:text="
`${recipe.notes.length} ${
recipe.notes.length == 1 ? 'Note' : 'Notes'
}`
"
textWrap="true"
/>
</StackLayout>
<StackLayout
class="overviewItem"
row="1"
col="1"
@tap="selectedTabIndex = 4"
>
<Label class="bx" :text="icon.source" />
<Label
class="itemCount"
:text="
`${recipe.references.length} ${
recipe.references.length == 1
? 'Reference'
: 'References'
}`
"
textWrap="true"
/>
</StackLayout>
</GridLayout>
</StackLayout>
</StackLayout>
</ScrollView>
</TabViewItem>
<TabViewItem title="Ingredients">
<ScrollView scrollBarIndicatorVisible="false">
<Label
<GridLayout
v-if="!recipe.ingredients.length"
class="noResults"
text="Click the edit button to add ingredients to this recipe"
textWrap="true"
/>
<StackLayout v-else padding="16 16 124">
rows="*"
columns="*"
class="emptyState"
>
<StackLayout col="0" row="0" class="noResult">
<Label class="bx icon" :text="icon.item" textWrap="true" />
<StackLayout orientation="horizontal" class="title orkm">
<Label text="Use the " />
<Label class="bx" :text="icon.edit" />
<Label text=" button" />
</StackLayout>
<Label text="to add some ingredients" textWrap="true" />
</StackLayout>
</GridLayout>
<StackLayout v-else padding="16 16 134">
<AbsoluteLayout class="inputField">
<TextField
width="165"
v-model="portionScale"
width="50%"
v-model="yieldMultiplier"
keyboardType="number"
/>
<Label top="0" class="fieldLabel" text="Set portion size" />
</AbsoluteLayout>
<StackLayout margin="24 0 8 0">
<Label
class="view-title orkm"
top="0"
class="fieldLabel"
:text="`Required ${recipe.yield.unit.toLowerCase()}`"
/>
</AbsoluteLayout>
<StackLayout margin="24 0 16 0">
<Label
class="title orkm"
:text="
`Ingredients for ${portionScale}${
portionScale > 1
? ' portions'
: portionScale == 0
? '1 portion'
: ' portion'
}`
`Ingredients for ${
yieldMultiplier ? yieldMultiplier : 1
} ${recipe.yield.unit.toLowerCase()}`
"
textWrap="true"
/>
<Label
class="view-ingredient"
v-for="(item, index) in recipe.ingredients"
:key="index"
</StackLayout>
<StackLayout
v-for="(item, index) in recipe.ingredients"
:key="index"
>
<check-box
v-if="filterTrylater"
class="ingredient-check"
checkPadding="16"
:fillColor="`${isLightMode ? '#ff5722' : '#ff7043'}`"
:text="
`${roundedQuantity(item.quantity)}${
item.unit ? ' ' + item.unit : ''
} ${item.item}`
`${
roundedQuantity(item.quantity)
? roundedQuantity(item.quantity) + ' '
: ''
}${roundedQuantity(item.quantity) ? item.unit + ' ' : ''}${
item.item
}`
"
/>
<Label
v-else
class="ingredient"
textWrap="true"
:text="
`${
roundedQuantity(item.quantity)
? roundedQuantity(item.quantity) + ' '
: ''
}${roundedQuantity(item.quantity) ? item.unit + ' ' : ''}${
item.item
}`
"
/>
</StackLayout>
</StackLayout>
@ -146,12 +261,22 @@
</TabViewItem>
<TabViewItem title="Instructions">
<ScrollView scrollBarIndicatorVisible="false">
<Label
<GridLayout
v-if="!recipe.instructions.length"
class="noResults"
text="Click the edit button to add instructions to this recipe"
textWrap="true"
/>
rows="*"
columns="*"
class="emptyState"
>
<StackLayout col="0" row="0" class="noResult">
<Label class="bx icon" :text="icon.step" textWrap="true" />
<StackLayout orientation="horizontal" class="title orkm">
<Label text="Use the " />
<Label class="bx" :text="icon.edit" />
<Label text=" button" />
</StackLayout>
<Label text="to add some instructions" textWrap="true" />
</StackLayout>
</GridLayout>
<StackLayout v-else padding="32 16 132">
<GridLayout
columns="auto ,*"
@ -161,10 +286,9 @@
<Label
col="0"
colSpan="2"
class="view-instruction"
class="instruction"
:class="{
instructionWOBorder:
index === recipe.instructions.length - 1,
noBorder: index === recipe.instructions.length - 1,
}"
:text="instruction"
textWrap="true"
@ -172,7 +296,7 @@
<Label
verticalAlignment="top"
horizontalAlignment="center"
class="view-count orkb"
class="count orkb"
col="0"
:text="index + 1"
/>
@ -182,12 +306,22 @@
</TabViewItem>
<TabViewItem title="Notes">
<ScrollView scrollBarIndicatorVisible="false">
<Label
<GridLayout
v-if="!recipe.notes.length"
class="noResults"
text="Click the edit button to add notes to this recipe"
textWrap="true"
/>
rows="*"
columns="*"
class="emptyState"
>
<StackLayout col="0" row="0" class="noResult">
<Label class="bx icon" :text="icon.note" textWrap="true" />
<StackLayout orientation="horizontal" class="title orkm">
<Label text="Use the " />
<Label class="bx" :text="icon.edit" />
<Label text=" button" />
</StackLayout>
<Label text="to add some notes" textWrap="true" />
</StackLayout>
</GridLayout>
<StackLayout v-else padding="32 16 132">
<GridLayout
columns="auto ,*"
@ -197,14 +331,14 @@
<Label
col="0"
colSpan="2"
class="view-note"
class="note"
:text="note"
textWrap="true"
/>
<Label
verticalAlignment="top"
horizontalAlignment="center"
class="view-count note orkb"
class="count square orkb"
col="0"
:text="index + 1"
/>
@ -214,12 +348,22 @@
</TabViewItem>
<TabViewItem title="References">
<ScrollView scrollBarIndicatorVisible="false">
<Label
<GridLayout
v-if="!recipe.references.length"
class="noResults"
text="Click the edit button to add references to this recipe"
textWrap="true"
/>
rows="*"
columns="*"
class="emptyState"
>
<StackLayout col="0" row="0" class="noResult">
<Label class="bx icon" :text="icon.source" textWrap="true" />
<StackLayout orientation="horizontal" class="title orkm">
<Label text="Use the " />
<Label class="bx" :text="icon.edit" />
<Label text=" button" />
</StackLayout>
<Label text="to add some references" textWrap="true" />
</StackLayout>
</GridLayout>
<StackLayout v-else padding="10 0 132">
<StackLayout
v-for="(reference, index) in recipe.references"
@ -228,27 +372,27 @@
<GridLayout
v-if="isValidURL(reference)"
columns="*, auto"
class="view-reference-container"
class="referenceItem"
androidElevation="1"
@longPress="copyURL($event, reference)"
>
<Label
col="0"
verticalAlignment="center"
class="view-reference"
class="recipeLink"
:text="reference"
textWrap="false"
@tap="openURL($event, reference)"
/>
<Label
col="1"
class="view-copyReference bx"
:text="icon.copy"
@tap="copyURL($event, reference)"
class="bx"
:text="icon.source"
@tap="openURL($event, reference)"
/>
</GridLayout>
<Label
v-else
class="view-reference-text"
class="recipeText"
:text="reference"
textWrap="true"
/>
@ -262,7 +406,7 @@
row="1"
col="1"
class="bx fab-button"
:text="icon.unchecked"
:text="icon.check"
@tap="recipeTried"
v-if="filterTrylater"
/>
@ -282,12 +426,19 @@
</template>
<script>
import { Screen, Utils, ImageSource, Device, File } from "@nativescript/core"
import {
Screen,
Utils,
ImageSource,
Device,
File,
Color,
} from "@nativescript/core"
import { Feedback, FeedbackType, FeedbackPosition } from "nativescript-feedback"
import * as application from "tns-core-modules/application"
import * as Toast from "nativescript-toast"
import * as SocialShare from "nativescript-social-share-ns-7"
import { setText } from "nativescript-clipboard"
import { Application } from "@nativescript/core"
import { mapState, mapActions } from "vuex"
@ -304,9 +455,10 @@ export default {
data() {
return {
busy: false,
portionScale: 1,
yieldMultiplier: 1,
recipe: null,
showFab: false,
selectedTabIndex: 0,
}
},
computed: {
@ -314,11 +466,15 @@ export default {
screenWidth() {
return Screen.mainScreen.widthDIPs
},
isPortionScalePositive() {
return this.portionScale && !isNaN(this.portionScale)
? parseFloat(this.portionScale)
isYieldMultiplierPositive() {
return this.yieldMultiplier && !isNaN(this.yieldMultiplier)
? parseFloat(this.yieldMultiplier)
: 1
},
isLightMode() {
console.log(Application.systemAppearance())
return Application.systemAppearance() === "light"
},
},
methods: {
...mapActions(["toggleStateAction", "setCurrentComponentAction"]),
@ -328,42 +484,38 @@ export default {
setTimeout((e) => {
this.setCurrentComponentAction("ViewRecipe")
}, 500)
this.portionScale = this.recipe.portionSize
this.yieldMultiplier = this.recipe.yield.quantity
this.showFab = true
this.showInfo()
},
niceDates(time) {
let date = new Date(time)
let diff = (new Date().getTime() - date.getTime()) / 1000
console.log(diff)
let lastTried = new Date(time).getTime()
let now = new Date().getTime()
let midnight = new Date().setHours(0, 0, 0, 0)
let diff = (now - lastTried) / 1000
let dayDiff = Math.ceil(diff / 86400)
console.log(dayDiff)
if (isNaN(dayDiff) || dayDiff < 0) return ""
return (
(dayDiff == 0 &&
((diff < 60 && "just now") ||
(diff < 120 && "1 minute ago") ||
(diff < 3600 && Math.floor(diff / 60) + " minutes ago") ||
(diff < 7200 && "1 hour ago") ||
(diff < 86400 && Math.floor(diff / 3600) + " hours ago"))) ||
(diff < 86400 && lastTried > midnight && "today") ||
(dayDiff == 1 && "yesterday") ||
(dayDiff < 7 && dayDiff + " days ago") ||
(dayDiff < 31 && Math.ceil(dayDiff / 7) + " week(s) ago") ||
(dayDiff > 30 &&
dayDiff < 365 &&
Math.round(dayDiff / 30) + " month(s) ago") ||
(dayDiff > 364 && Math.round(dayDiff / 365) + " year(s) ago")
(dayDiff < 31 && Math.round(dayDiff / 7) + " week(s) ago") ||
(dayDiff < 366 && Math.round(dayDiff / 30) + " month(s) ago") ||
(dayDiff > 365 && "long time ago")
)
},
selectedIndexChange(args) {
this.selectedTabIndex = args.object.selectedIndex
},
showInfo() {
let feedback = new Feedback()
feedback.show({
type: FeedbackType.Info,
message: `You tried this recipe ${this.niceDates(
this.recipe.triedOn
title: `You tried this recipe ${this.niceDates(
this.recipe.lastTried
)}!`,
titleColor: new Color(`${this.isLightMode ? "#fff" : "#111"}`),
backgroundColor: new Color(
`${this.isLightMode ? "#ff5722" : "#ff7043"}`
),
})
},
highlight(args) {
@ -374,8 +526,8 @@ export default {
roundedQuantity(quantity) {
return (
Math.round(
(quantity / this.recipe.portionSize) *
this.isPortionScalePositive *
(quantity / this.recipe.yield.quantity) *
this.isYieldMultiplierPositive *
100
) / 100
)
@ -399,14 +551,14 @@ export default {
shareRecipe() {
let overview = `${
this.recipe.title
} Recipe\n\nPreparation time: ${this.getTime(
this.recipe.prepTime
)}\nCooking time: ${this.getTime(this.recipe.cookTime)}\n`
} Recipe\n\nApprox. cooking time: ${this.formattedTime(
this.recipe.timeRequired
)}\n`
let shareContent = overview
if (this.recipe.ingredients.length) {
let ingredients = `\n\nIngredients for ${this.recipe.portionSize} ${
this.recipe.portionSize === 1 ? "postion:" : "portions:"
}\n\n`
let ingredients = `\n\nIngredients for ${
this.yieldMultiplier
} ${this.recipe.yield.unit.toLowerCase()}\n\n`
this.recipe.ingredients.forEach((e) => {
ingredients += `- ${this.roundedQuantity(e.quantity)} ${e.unit} ${
e.item
@ -417,7 +569,7 @@ export default {
if (this.recipe.instructions.length) {
let instructions = `\n\nInstructions:\n\n`
this.recipe.instructions.forEach((e, i) => {
instructions += `${i + 1}. ${e}\n\n`
instructions += `Step ${i + 1}: ${e}\n\n`
})
shareContent += instructions
}
@ -445,12 +597,13 @@ export default {
"How would you like to share this recipe?"
)
},
toggle(key) {
toggle(key, setDate) {
this.toggleStateAction({
index: this.recipeIndex,
id: this.recipeID,
recipe: this.recipe,
key,
setDate,
})
},
toggleFavorite() {
@ -471,14 +624,14 @@ export default {
this.toggle("tried")
},
recipeTried() {
this.toggle("tried")
this.toggle("tried", true)
this.$navigateBack()
},
getTime(time) {
formattedTime(time) {
let t = time.split(":")
let h = t[0]
let m = t[1]
return h !== "00" ? `${h}h ${m}m` : `${m}m`
let h = parseInt(t[0])
let m = parseInt(t[1])
return h ? (m ? `${h}h ${m}m` : `${h}h`) : `${m}m`
},
isValidURL(string) {
let pattern = new RegExp("^https?|www", "ig")
@ -499,6 +652,7 @@ export default {
},
mounted() {
this.showFab = true
setTimeout((e) => this.recipe.tried && this.showInfo(), 2000)
},
}
</script>

View file

@ -1,7 +1,6 @@
<template>
<Page>
<StackLayout class="dialogContainer" :class="isLightMode">
<!-- :class="isLightTheme ? 'light' : 'dark'" -->
<Label class="dialogTitle orkm" :text="title" />
<ListView
width="100%"
@ -11,22 +10,20 @@
separatorColor="transparent"
>
<v-template>
<StackLayout class="actionItem">
<Label :text="item" />
</StackLayout>
<Label class="actionItem" :text="item" />
</v-template>
</ListView>
<GridLayout rows="auto" columns="auto, *, auto">
<GridLayout rows="auto" columns="auto, *, auto" class="actionsContainer">
<Label
v-if="action"
col="0"
class="cancel orkm pull-left"
class="action orkm pull-left"
:text="action"
@tap="$modal.close(action)"
/>
<Label
col="2"
class="cancel orkm pull-right"
class="action orkm pull-right"
text="CANCEL"
@tap="$modal.close(false)"
/>

View file

@ -2,23 +2,30 @@
<Page>
<StackLayout class="dialogContainer" :class="isLightMode">
<Label class="dialogTitle orkm" :text="title" />
<Label v-if="description" class="dialogDescription" :text="description" textWrap="true" />
<StackLayout
orientation="horizontal"
<Label
v-if="description"
class="dialogDescription"
:text="description"
textWrap="true"
/>
<GridLayout
rows="auto"
columns="*, auto, 32, auto"
class="actionsContainer"
horizontalAlignment="right"
>
<Label
col="1"
class="action orkm"
:text="cancelButtonText"
@tap="$modal.close(false)"
/>
<Label
col="3"
class="action orkm"
:text="okButtonText"
@tap="$modal.close(true)"
/>
</StackLayout>
</GridLayout>
</StackLayout>
</Page>
</template>

View file

@ -0,0 +1,132 @@
<template>
<Page>
<StackLayout class="dialogContainer" :class="isLightMode">
<Label class="dialogTitle orkm" :text="title" />
<StackLayout
class="dialogListPicker"
orientation="horizontal"
horizontalAlignment="center"
>
<ListPicker
ref="hrPicker"
:items="hrs"
:selectedIndex="hrIndex"
@selectedIndexChange="setHrs"
></ListPicker>
<Label
verticalAlignment="center"
class="okrb"
text=":"
textWrap="false"
/>
<ListPicker
ref="minPicker"
:items="mins"
:selectedIndex="minIndex"
@selectedIndexChange="setMins"
></ListPicker>
</StackLayout>
<GridLayout
rows="auto"
columns="*, auto, 32, auto"
class="actionsContainer"
>
<Label
col="1"
class="action orkm"
text="CANCEL"
@tap="$modal.close(false)"
/>
<Label
col="3"
class="action orkm"
:text="action"
@tap="$modal.close(selectedTime)"
/>
</GridLayout>
</StackLayout>
</Page>
</template>
<script>
import { Application } from "@nativescript/core"
export default {
props: ["title", "selectedHr", "selectedMin", "action"],
data() {
return {
hrs: [
"00",
"01",
"02",
"03",
"04",
"05",
"06",
"07",
"08",
"09",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"20",
"21",
"22",
"23",
],
mins: [
"00",
"01",
"02",
"03",
"04",
"05",
"06",
"07",
"08",
"09",
"10",
"15",
"20",
"25",
"30",
"35",
"40",
"45",
"50",
"55",
],
selectedHrs: "00",
selectedMins: "00",
}
},
computed: {
hrIndex() {
return this.hrs.indexOf(this.selectedHr)
},
minIndex() {
return this.mins.indexOf(this.selectedMin)
},
isLightMode() {
return Application.systemAppearance()
},
selectedTime() {
return this.selectedHrs + ":" + this.selectedMins
},
},
methods: {
setHrs(args) {
this.selectedHrs = this.hrs[args.object.selectedIndex]
},
setMins(args) {
this.selectedMins = this.mins[args.object.selectedIndex]
},
},
}
</script>

View file

@ -2,27 +2,38 @@
<Page>
<StackLayout class="dialogContainer" :class="isLightMode">
<Label class="dialogTitle orkm" :text="title" />
<StackLayout class="dialogInputField">
<StackLayout class="dialogInput">
<TextField
@loaded="focusField"
:hint="hint"
v-model="category"
autocapitalizationType="words"
/>
</StackLayout>
<StackLayout orientation="horizontal" horizontalAlignment="right">
<Label class="action orkm" text="CANCEL" @tap="$modal.close(false)" />
<GridLayout
rows="auto"
columns="*, auto, 32, auto"
class="actionsContainer"
>
<Label
col="1"
class="action orkm"
text="CANCEL"
@tap="$modal.close(false)"
/>
<Label
col="3"
class="action orkm"
:text="action"
@tap="$modal.close(category)"
/>
</StackLayout>
</GridLayout>
</StackLayout>
</Page>
</template>
<script>
import { Application } from "@nativescript/core"
import { Application, Utils } from "@nativescript/core"
export default {
props: ["title", "hint", "action"],
data() {
@ -35,5 +46,11 @@ export default {
return Application.systemAppearance()
},
},
methods: {
focusField(args) {
args.object.focus()
setTimeout((e) => Utils.ad.showSoftInput(args.object.android), 1)
},
},
}
</script>

Binary file not shown.

View file

@ -12,11 +12,13 @@ Vue.registerElement(
"RadSideDrawer",
() => require("nativescript-ui-sidedrawer").RadSideDrawer
)
// Vue.registerElement(
// 'Fab',
// () => require('@nstudio/nativescript-floatingactionbutton').Fab
// );
import { CheckBox } from "@nstudio/nativescript-checkbox"
Vue.registerElement("CheckBox", () => CheckBox, {
model: {
prop: "checked",
event: "checkedChange",
},
})
if (TNS_ENV !== "production") {
// Vue.use(VueDevtools)

View file

@ -2,124 +2,182 @@ import Vue from "vue"
import Vuex from "vuex"
import { Couchbase } from "nativescript-couchbase-plugin"
const recipesDB = new Couchbase("enrecipes")
const categoriesDB = new Couchbase("categories")
const categoriesDB = new Couchbase("userCategories")
const yieldUnitsDB = new Couchbase("userYieldUnits")
Vue.use(Vuex)
let defaultCategories = [
"Appetizers",
"BBQ",
"Beverages",
"Breads",
"Breakfast",
"Desserts",
"Dinner",
"Drinks",
"Healthy",
"Lunch",
"Main dishes",
"Meat",
"Noodles",
"Pasta",
"Poultry",
"Rice",
"Salads",
"Sauces",
"Seafood",
"Side dishes",
"Snacks",
"Soups",
"Undefined",
"Vegan",
"Vegetarian",
]
let defaultYieldUnits = [
"Serving",
"Piece",
"Teaspoon",
"Tablespoon",
"Fluid Ounce",
"Ounce",
"Pound",
"Gram",
"Kilogram",
"Cup",
"Gallon",
"Millilitre",
"Litre",
"Roll",
"Pattie",
"Loaf",
]
export default new Vuex.Store({
state: {
recipes: [
{
imageSrc: null,
title: "Mushroom & Spinach Risotto",
category: "Lunch",
prepTime: "00:25",
cookTime: "00:45",
portionSize: "8",
ingredients: [
{
item: "reduced-sodium chicken broth",
unit: "cup",
quantity: "5.25",
},
{ item: "sliced fresh mushrooms", unit: "cup", quantity: "2.5" },
{ item: "medium onion, finely chopped", unit: "unit", quantity: "1" },
{ item: "butter", unit: "Tbsp", quantity: "3" },
{ item: "Garlic", unit: "unit", quantity: "3" },
{
item: "white wine or reduced-sodium chicken broth",
unit: "cup",
quantity: ".75",
},
{ item: "heavy whipping cream", unit: "cup", quantity: "1" },
{ item: "uncooked arborio rice", unit: "cup", quantity: "1.75" },
{ item: "olive oil", unit: "Tbsp", quantity: "2" },
{
item: "frozen chopped spinach, thawed and squeezed dry",
unit: "cup",
quantity: "1.5",
},
{ item: "pepper", unit: "tsp", quantity: ".5" },
{ item: "Salt", unit: "tsp", quantity: ".25" },
{ item: "grated Parmesan cheese", unit: "cup", quantity: "1" },
],
instructions: [
"In a large saucepan, heat broth and keep warm. In a large skillet, saute mushrooms and onion in butter until tender. Add garlic; cook 1 minute longer. Stir in wine. Bring to a boil; cook until liquid is reduced by half. Add cream; cook and stir over medium heat until slightly thickened",
"In a large saucepan, saute rice in oil for 2-3 minutes or until rice is lightly browned. Stir in 1/2 cup hot broth. Reduce heat; cook and stir for 20 minutes or until broth is absorbed.",
"Continue adding hot broth, 1/2 cup at a time, and stirring until all the broth has been absorbed and rice is tender but firm. Add the mushroom mixture, spinach, pepper, salt and grated Parmesan cheese; cook and stir until heated through. If desired, sprinkle with parsley and shaved Parmesan cheese. Serve immediately.",
],
notes: [
"Nutrition Facts\n3/4 cup: 409 calories, 22g fat (12g saturated fat), 61mg cholesterol, 667mg sodium, 41g carbohydrate (3g sugars, 2g fiber), 11g protein.",
],
references: [
"https://www.tasteofhome.com/recipes/mushroom-spinach-risotto/",
],
isFavorite: false,
tried: true,
triedOn: "2020-10-28T18:19:06.528Z",
lastModified: "2020-10-28T06:19:06.528Z",
id: "57qm8oqxdr",
},
// {
// imageSrc: null,
// title: "Mushroom & Spinach Risotto",
// category: "Lunch",
// timeRequired: "01:45",
// yield: {
// quantity: 1,
// unit: "Serving",
// },
// ingredients: [
// {
// item: "reduced-sodium chicken broth",
// unit: "cup",
// quantity: "5.25",
// },
// { item: "sliced fresh mushrooms", unit: "cup", quantity: "2.5" },
// { item: "medium onion, finely chopped", unit: "unit", quantity: "1" },
// { item: "butter", unit: "Tbsp", quantity: "3" },
// { item: "Garlic", unit: "unit", quantity: "3" },
// {
// item: "white wine or reduced-sodium chicken broth",
// unit: "l",
// quantity: ".75",
// },
// { item: "heavy whipping cream", unit: "cup", quantity: "1" },
// { item: "uncooked arborio rice", unit: "cup", quantity: "1.75" },
// { item: "olive oil", unit: "Tbsp", quantity: "2" },
// {
// item: "frozen chopped spinach, thawed and squeezed dry",
// unit: "cup",
// quantity: "1.5",
// },
// { item: "pepper", unit: "tsp", quantity: ".5" },
// { item: "Salt", unit: "tsp", quantity: ".25" },
// { item: "grated Parmesan cheese", unit: "cup", quantity: "1" },
// ],
// instructions: [
// "In a large saucepan, heat broth and keep warm. In a large skillet, saute mushrooms and onion in butter until tender. Add garlic; cook 1 minute longer. Stir in wine. Bring to a boil; cook until liquid is reduced by half. Add cream; cook and stir over medium heat until slightly thickened",
// "In a large saucepan, saute rice in oil for 2-3 minutes or until rice is lightly browned. Stir in 1/2 cup hot broth. Reduce heat; cook and stir for 20 minutes or until broth is absorbed.",
// "Continue adding hot broth, 1/2 cup at a time, and stirring until all the broth has been absorbed and rice is tender but firm. Add the mushroom mixture, spinach, pepper, salt and grated Parmesan cheese; cook and stir until heated through. If desired, sprinkle with parsley and shaved Parmesan cheese. Serve immediately.",
// ],
// notes: [
// "Nutrition Facts\n3/4 cup: 409 calories, 22g fat (12g saturated fat), 61mg cholesterol, 667mg sodium, 41g carbohydrate (3g sugars, 2g fiber), 11g protein.",
// ],
// references: [
// "https://www.tasteofhome.com/recipes/mushroom-spinach-risotto/",
// ],
// isFavorite: false,
// tried: false,
// lastTried: "2020-10-28T18:19:06.528Z",
// lastModified: "2020-10-28T06:19:06.528Z",
// id: "57qm8oqxdr",
// },
],
categories: [],
userCategories: [],
units: [
"unit",
"tsp",
"Tbsp",
"oz",
"tbsp",
"fl oz",
"cup",
"pt",
"qt",
"lb",
"gal",
"ml",
"L",
"l",
"oz",
"lb",
"mg",
"g",
"kg",
"mm",
"cm",
"m",
"in",
"°C",
"°F",
"clove",
"pinch",
"drop",
"dozen",
"stick",
"small",
"medium",
"large",
],
yieldUnits: [],
userYieldUnits: [],
icon: {
home: "\ued99",
heart: "\ued94",
heartOutline: "\uead6",
label: "\uedaf",
cog: "\ued05",
info: "\ueda7",
menu: "\ueb2a",
search: "\uebbc",
sort: "\ueb2b",
plus: "\ueb89",
close: "\uec4e",
image: "\ueae9",
addImage: "\ueae8",
back: "\ue988",
save: "\uee48",
camera: "\uecc2",
share: "\uee50",
edit: "\uee17",
theme: "\ued09",
folder: "\ued7c",
backup: "\uee48",
restore: "\ueadc",
link: "\ueb09",
file: "\ued60",
user: "\uee8e",
trash: "\uee83",
donate: "\ued41",
trylater: "\uec96",
trylaterOutline: "\ue9bb",
note: "\uee0a",
copy: "\uea51",
plusCircle: "\ueb8a",
unchecked: "\uea16",
checked: "\uece6",
telegram: "\ue95e",
home: "\ued3b",
heart: "\ued36",
heartOutline: "\uea6c",
label: "\ued51",
cog: "\ueca6",
info: "\ued49",
menu: "\ueac1",
search: "\ueb54",
sort: "\ueac2",
plus: "\ueb21",
plusCircle: "\ueb22",
close: "\uebe9",
image: "\uea7f",
food: "\ueb3e",
back: "\uea95",
save: "\uedeb",
camera: "\uec61",
share: "\uedf3",
edit: "\uedba",
theme: "\uecaa",
restore: "\uea72",
link: "\ueaa0",
file: "\ued02",
user: "\uee33",
trash: "\uee26",
donate: "\ueb4f",
trylater: "\uec31",
trylaterOutline: "\ue94a",
note: "\ueb04",
copy: "\ue9e6",
check: "\ue9ab",
telegram: "\ueec7",
time: "\ueba2",
item: "\ue99d",
step: "\ue948",
source: "\ueaa0",
},
currentComponent: "EnRecipes",
},
@ -132,58 +190,58 @@ export default new Vuex.Store({
},
initializeCategories(state) {
let isCategoriesStored = categoriesDB.query({ select: [] }).length
let cats
if (isCategoriesStored) {
cats = categoriesDB.getDocument("categories").categories
} else {
categoriesDB.createDocument(
{
categories: [
"Appetizers",
"BBQ",
"Beverages",
"Breads",
"Breakfast",
"Desserts",
"Dinner",
"Drinks",
"Healthy",
"Lunch",
"Main dishes",
"Meat",
"Noodles",
"Pasta",
"Poultry",
"Rice",
"Salads",
"Sauces",
"Seafood",
"Side dishes",
"Snacks",
"Soups",
"Undefined",
"Vegan",
"Vegetarian",
],
},
"categories"
state.userCategories = categoriesDB.getDocument(
"userCategories"
).userCategories
let categoriesWithRecipes = state.recipes.map((e) => e.category)
state.userCategories = state.userCategories.filter((e) =>
categoriesWithRecipes.includes(e)
)
cats = categoriesDB.getDocument("categories").categories
} else {
categoriesDB.createDocument({ userCategories: [] }, "userCategories")
}
cats.forEach((e) => state.categories.push(e))
state.categories = [...defaultCategories, ...state.userCategories]
state.categories.sort()
},
initializeYieldUnits(state) {
let isYieldUnitsStored = yieldUnitsDB.query({ select: [] }).length
if (isYieldUnitsStored) {
state.userYieldUnits = yieldUnitsDB.getDocument(
"userYieldUnits"
).userYieldUnits
let yieldUnitsWithRecipes = state.recipes.map((e) => e.yield.unit)
state.userYieldUnits = state.userYieldUnits.filter((e) =>
yieldUnitsWithRecipes.includes(e)
)
} else {
yieldUnitsDB.createDocument({ userYieldUnits: [] }, "userYieldUnits")
}
state.yieldUnits = [...defaultYieldUnits, ...state.userYieldUnits]
},
addRecipe(state, { id, recipe }) {
state.recipes.push(recipe)
recipesDB.createDocument(recipe, id)
},
addCategory(state, category) {
let a = state.categories.filter((e) => e === category).length
if (a == 0) {
state.categories.push(category)
state.categories.sort()
categoriesDB.updateDocument("categories", {
categories: [...state.categories],
let lowercase = state.categories.map((e) => e.toLowerCase())
if (lowercase.indexOf(category.toLowerCase()) == -1) {
state.userCategories.push(category)
categoriesDB.updateDocument("userCategories", {
userCategories: [...state.userCategories],
})
state.categories = [...defaultCategories, ...state.userCategories]
state.categories.sort()
}
},
addYieldUnit(state, unit) {
let lowercase = state.yieldUnits.map((e) => e.toLowerCase())
if (lowercase.indexOf(unit.toLowerCase()) == -1) {
state.userYieldUnits.push(unit)
yieldUnitsDB.updateDocument("userYieldUnits", {
userYieldUnits: [...state.userYieldUnits],
})
state.yieldUnits = [...defaultYieldUnits, ...state.userYieldUnits]
}
},
overwriteRecipe(state, { index, id, recipe }) {
@ -194,25 +252,27 @@ export default new Vuex.Store({
state.recipes.splice(index, 1)
recipesDB.deleteDocument(id)
},
toggleState(state, { index, id, recipe, key }) {
toggleState(state, { index, id, recipe, key, setDate }) {
state.recipes[index][key] = !state.recipes[index][key]
if (setDate) state.recipes[index].lastTried = new Date()
recipesDB.updateDocument(id, recipe)
},
setLastTriedDate(state, index) {
state.recipes[index].lastTried = new Date()
recipesDB.updateDocument(state.recipes[index].id, state.recipes[index])
},
setCurrentComponent(state, comp) {
state.currentComponent = comp
},
renameCategory(state, { current, updated }) {
let exists = state.categories.filter((e) => e === updated).length
state.categories.splice(state.categories.indexOf(current), 1)
// update recipes with updated category
if (!exists) {
state.categories.push(updated)
state.categories.sort()
categoriesDB.updateDocument("categories", {
categories: [...state.categories],
let lowercase = state.categories.map((e) => e.toLowerCase())
if (lowercase.indexOf(updated.toLowerCase()) == -1) {
state.userCategories.push(updated)
categoriesDB.updateDocument("userCategories", {
userCategories: [...state.userCategories],
})
state.categories = [...defaultCategories, ...state.userCategories]
state.categories.sort()
}
state.recipes.forEach((e, i) => {
if (e.category == current) {
@ -231,12 +291,18 @@ export default new Vuex.Store({
initializeCategories({ commit }) {
commit("initializeCategories")
},
initializeYieldUnits({ commit }) {
commit("initializeYieldUnits")
},
addRecipeAction({ commit }, recipe) {
commit("addRecipe", recipe)
},
addCategoryAction({ commit }, category) {
commit("addCategory", category)
},
addYieldUnitAction({ commit }, yieldUnit) {
commit("addYieldUnit", yieldUnit)
},
overwriteRecipeAction({ commit }, updatedRecipe) {
commit("overwriteRecipe", updatedRecipe)
},
@ -246,6 +312,9 @@ export default new Vuex.Store({
toggleStateAction({ commit }, toggledRecipe) {
commit("toggleState", toggledRecipe)
},
setLastTriedDateAction({ commit }, index) {
commit("setLastTriedDate", index)
},
setCurrentComponentAction({ commit }, comp) {
commit("setCurrentComponent", comp)
},

22
app/worker.service.js Normal file
View file

@ -0,0 +1,22 @@
const workers = []
export class WorkerService {
constructor() {}
initImageProcessor() {
if (this.imageProcessor) {
return this.imageProcessor
}
const ImageProcessor = require("nativescript-worker-loader!./workers/ImageProcessor.worker.js")
this.imageProcessor = new ImageProcessor()
workers.push(this.imageProcessor)
return this.imageProcessor
}
}
if (module.hot) {
module.hot.dispose(() => {
workers.forEach((w) => {
w.terminate()
})
})
}

View file

@ -0,0 +1,12 @@
require("tns-core-modules/globals")
import { ImageSource } from "@nativescript/core"
global.onmessage = function({ data }) {
let imgFile = data.imgFile
let imgSavedToPath = data.imgSavedToPath
ImageSource.fromFile(imgFile).then((imgData) => {
if (imgData.saveToFile(imgSavedToPath, "jpg")) {
global.postMessage("savedToFile")
}
})
}

View file

@ -1,11 +1,12 @@
import { NativeScriptConfig } from '@nativescript/core'
import { NativeScriptConfig } from "@nativescript/core"
export default {
id: 'com.vishnuraghav.enrecipes',
appResourcesPath: 'app/App_Resources',
id: "com.vishnuraghav.enrecipes",
appResourcesPath: "App_Resources",
android: {
v8Flags: '--expose_gc',
markingMode: 'none',
v8Flags: "--expose_gc",
markingMode: "none",
codeCache: true,
},
appPath: 'app',
appPath: "app",
} as NativeScriptConfig

5
package-lock.json generated
View file

@ -1203,6 +1203,11 @@
"mkdirp": "^1.0.4"
}
},
"@nstudio/nativescript-checkbox": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@nstudio/nativescript-checkbox/-/nativescript-checkbox-2.0.4.tgz",
"integrity": "sha512-ypIGAHxDE/2o3CzYohSdypdhiw4GjMcZ3H/qtF4z97HMcMqj+g5bYPDC9cRH97qgAez8jf/z3UX5OzOtnrNxug=="
},
"@types/anymatch": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",

View file

@ -12,6 +12,7 @@
"@nativescript/datetimepicker": "^2.0.4",
"@nativescript/theme": "^3.0.0",
"@nativescript/webpack": "3.0.0",
"@nstudio/nativescript-checkbox": "^2.0.4",
"nativescript-clipboard": "^2.0.0",
"nativescript-couchbase-plugin": "^0.9.6",
"nativescript-feedback": "^2.0.0",