This commit is contained in:
Vishnu Raghav B 2020-11-10 23:58:48 +05:30
parent 16935b62d2
commit cff2dea334
61 changed files with 12239 additions and 1572 deletions

8595
.update_backup/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,46 @@
{
"name": "enrecipes",
"version": "1.0.0",
"description": "A native application built with NativeScript-Vue",
"author": "Vishnu Raghav <design@vishnuraghav.com>",
"license": "GPL",
"scripts": {
"run": "ns run android"
},
"dependencies": {
"@nativescript-community/perms": "^2.1.1",
"@nativescript-community/ui-material-activityindicator": "^5.0.30",
"@nativescript-community/ui-material-button": "^5.1.0",
"@nativescript-community/ui-material-floatingactionbutton": "^5.0.30",
"@nativescript-community/ui-material-progress": "^5.0.30",
"@nativescript-community/ui-material-ripple": "^5.0.30",
"@nativescript/core": "~7.0.0",
"@nativescript/imagepicker": "^1.0.0",
"@nativescript/social-share": "^2.0.1",
"@nativescript/theme": "^3.0.0",
"@nativescript/webpack": "3.0.0",
"@nativescript/zip": "^5.0.0",
"@nstudio/nativescript-checkbox": "^2.0.4",
"nativescript-clipboard": "^2.0.0",
"nativescript-couchbase-plugin": "^0.9.6",
"nativescript-feedback": "^2.0.0",
"nativescript-imagecropper": "^4.0.1",
"nativescript-plugin-filepicker": "^1.0.0",
"nativescript-toast": "^2.0.0",
"nativescript-ui-listview": "^9.0.4",
"nativescript-ui-sidedrawer": "^9.0.3",
"nativescript-vue": "^2.6.1",
"vuex": "^3.3.0"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@nativescript/android": "7.0.1",
"@types/node": "^14.0.27",
"babel-loader": "^8.1.0",
"nativescript-vue-template-compiler": "^2.6.0",
"node-sass": "^4.13.1",
"vue-loader": "^15.9.1"
},
"main": "main"
}

View file

@ -0,0 +1,369 @@
const { join, relative, resolve, sep } = require("path");
const webpack = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const TerserPlugin = require("terser-webpack-plugin");
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const NsVueTemplateCompiler = require("nativescript-vue-template-compiler");
const nsWebpack = require("@nativescript/webpack");
const nativescriptTarget = require("@nativescript/webpack/nativescript-target");
const { NativeScriptWorkerPlugin } = require("nativescript-worker-loader/NativeScriptWorkerPlugin");
const hashSalt = Date.now().toString();
module.exports = env => {
// Add your custom Activities, Services and other android app components here.
const appComponents = env.appComponents || [];
appComponents.push(...[
"@nativescript/core/ui/frame",
"@nativescript/core/ui/frame/activity",
]);
const platform = env && (env.android && "android" || env.ios && "ios" || env.platform);
if (!platform) {
throw new Error("You need to provide a target platform!");
}
const platforms = ["ios", "android"];
const projectRoot = __dirname;
if (env.platform) {
platforms.push(env.platform);
}
// Default destination inside platforms/<platform>/...
const dist = resolve(projectRoot, nsWebpack.getAppPath(platform, projectRoot));
const {
// The 'appPath' and 'appResourcesPath' values are fetched from
// the nsconfig.json configuration file.
appPath = "app",
appResourcesPath = "app/App_Resources",
// You can provide the following flags when running 'tns run android|ios'
snapshot, // --env.snapshot
production, // --env.production
report, // --env.report
hmr, // --env.hmr
sourceMap, // --env.sourceMap
hiddenSourceMap, // --env.hiddenSourceMap
unitTesting, // --env.unitTesting
testing, // --env.testing
verbose, // --env.verbose
snapshotInDocker, // --env.snapshotInDocker
skipSnapshotTools, // --env.skipSnapshotTools
compileSnapshot // --env.compileSnapshot
} = env;
const useLibs = compileSnapshot;
const isAnySourceMapEnabled = !!sourceMap || !!hiddenSourceMap;
const externals = nsWebpack.getConvertedExternals(env.externals);
const mode = production ? "production" : "development"
const appFullPath = resolve(projectRoot, appPath);
const hasRootLevelScopedModules = nsWebpack.hasRootLevelScopedModules({ projectDir: projectRoot });
let coreModulesPackageName = "tns-core-modules";
const alias = env.alias || {};
alias['~/package.json'] = resolve(projectRoot, 'package.json');
alias['~'] = appFullPath;
alias['@'] = appFullPath;
alias['vue'] = 'nativescript-vue';
if (hasRootLevelScopedModules) {
coreModulesPackageName = "@nativescript/core";
alias["tns-core-modules"] = coreModulesPackageName;
}
const appResourcesFullPath = resolve(projectRoot, appResourcesPath);
const copyIgnore = { ignore: [`${relative(appPath, appResourcesFullPath)}/**`] };
const entryModule = nsWebpack.getEntryModule(appFullPath, platform);
const entryPath = `.${sep}${entryModule}`;
const entries = env.entries || {};
entries.bundle = entryPath;
const areCoreModulesExternal = Array.isArray(env.externals) && env.externals.some(e => e.indexOf("@nativescript") > -1);
if (platform === "ios" && !areCoreModulesExternal && !testing) {
entries["tns_modules/@nativescript/core/inspector_modules"] = "inspector_modules";
};
console.log(`Bundling application for entryPath ${entryPath}...`);
let sourceMapFilename = nsWebpack.getSourceMapFilename(hiddenSourceMap, __dirname, dist);
const itemsToClean = [`${dist}/**/*`];
if (platform === "android") {
itemsToClean.push(`${join(projectRoot, "platforms", "android", "app", "src", "main", "assets", "snapshots")}`);
itemsToClean.push(`${join(projectRoot, "platforms", "android", "app", "build", "configurations", "nativescript-android-snapshot")}`);
}
nsWebpack.processAppComponents(appComponents, platform);
const config = {
mode: mode,
context: appFullPath,
externals,
watchOptions: {
ignored: [
appResourcesFullPath,
// Don't watch hidden files
"**/.*",
],
},
target: nativescriptTarget,
// target: nativeScriptVueTarget,
entry: entries,
output: {
pathinfo: false,
path: dist,
sourceMapFilename,
libraryTarget: "commonjs2",
filename: "[name].js",
globalObject: "global",
hashSalt
},
resolve: {
extensions: [".vue", ".ts", ".js", ".scss", ".css"],
// Resolve {N} system modules from @nativescript/core
modules: [
resolve(__dirname, `node_modules/${coreModulesPackageName}`),
resolve(__dirname, "node_modules"),
`node_modules/${coreModulesPackageName}`,
"node_modules",
],
alias,
// resolve symlinks to symlinked modules
symlinks: true,
},
resolveLoader: {
// don't resolve symlinks to symlinked loaders
symlinks: false,
},
node: {
// Disable node shims that conflict with NativeScript
"http": false,
"timers": false,
"setImmediate": false,
"fs": "empty",
"__dirname": false,
},
devtool: hiddenSourceMap ? "hidden-source-map" : (sourceMap ? "inline-source-map" : "none"),
optimization: {
runtimeChunk: "single",
noEmitOnErrors: true,
splitChunks: {
cacheGroups: {
vendor: {
name: "vendor",
chunks: "all",
test: (module) => {
const moduleName = module.nameForCondition ? module.nameForCondition() : '';
return /[\\/]node_modules[\\/]/.test(moduleName) ||
appComponents.some(comp => comp === moduleName);
},
enforce: true,
},
},
},
minimize: Boolean(production),
minimizer: [
new TerserPlugin({
parallel: true,
cache: true,
sourceMap: isAnySourceMapEnabled,
terserOptions: {
output: {
comments: false,
semicolons: !isAnySourceMapEnabled
},
compress: {
// The Android SBG has problems parsing the output
// when these options are enabled
'collapse_vars': platform !== "android",
sequences: platform !== "android",
},
keep_fnames: true,
},
}),
],
},
module: {
rules: [{
include: [join(appFullPath, entryPath + ".js"), join(appFullPath, entryPath + ".ts")],
use: [
// Require all Android app components
platform === "android" && {
loader: "@nativescript/webpack/helpers/android-app-components-loader",
options: { modules: appComponents },
},
{
loader: "@nativescript/webpack/bundle-config-loader",
options: {
registerPages: true, // applicable only for non-angular apps
loadCss: !snapshot, // load the application css if in debug mode
unitTesting,
appFullPath,
projectRoot,
ignoredFiles: nsWebpack.getUserDefinedEntries(entries, platform)
},
},
].filter(loader => Boolean(loader)),
},
{
test: /[\/|\\]app\.css$/,
use: [
'@nativescript/webpack/helpers/style-hot-loader',
{
loader: "@nativescript/webpack/helpers/css2json-loader",
options: { useForImports: true }
},
],
},
{
test: /[\/|\\]app\.scss$/,
use: [
'@nativescript/webpack/helpers/style-hot-loader',
{
loader: "@nativescript/webpack/helpers/css2json-loader",
options: { useForImports: true }
},
'sass-loader',
],
},
{
test: /\.css$/,
exclude: /[\/|\\]app\.css$/,
use: [
'@nativescript/webpack/helpers/style-hot-loader',
'@nativescript/webpack/helpers/apply-css-loader.js',
{ loader: "css-loader", options: { url: false } },
],
},
{
test: /\.scss$/,
exclude: /[\/|\\]app\.scss$/,
use: [
'@nativescript/webpack/helpers/style-hot-loader',
'@nativescript/webpack/helpers/apply-css-loader.js',
{ loader: "css-loader", options: { url: false } },
'sass-loader',
],
},
{
test: /\.js$/,
loader: 'babel-loader',
},
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
allowTsInNodeModules: true,
compilerOptions: {
declaration: false
},
getCustomTransformers: (program) => ({
before: [
require("@nativescript/webpack/transformers/ns-transform-native-classes").default
]
})
},
},
{
test: /\.vue$/,
loader: "vue-loader",
options: {
compiler: NsVueTemplateCompiler,
},
},
],
},
plugins: [
// ... Vue Loader plugin omitted
// make sure to include the plugin!
new VueLoaderPlugin(),
// Define useful constants like TNS_WEBPACK
new webpack.DefinePlugin({
"global.TNS_WEBPACK": "true",
"global.isAndroid": platform === 'android',
"global.isIOS": platform === 'ios',
"TNS_ENV": JSON.stringify(mode),
"process": "global.process"
}),
// Remove all files from the out dir.
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: itemsToClean,
verbose: !!verbose
}),
// Copy assets
new CopyWebpackPlugin({
patterns: [
{ from: 'assets/**', noErrorOnMissing: true, globOptions: { dot: false, ...copyIgnore } },
{ from: 'fonts/**', noErrorOnMissing: true, globOptions: { dot: false, ...copyIgnore } },
{ from: '**/*.+(jpg|png)', noErrorOnMissing: true, globOptions: { dot: false, ...copyIgnore } }
],
}),
new nsWebpack.GenerateNativeScriptEntryPointsPlugin("bundle"),
// For instructions on how to set up workers with webpack
// check out https://github.com/nativescript/worker-loader
new NativeScriptWorkerPlugin(),
new nsWebpack.PlatformFSPlugin({
platform,
platforms,
}),
// Does IPC communication with the {N} CLI to notify events when running in watch mode.
new nsWebpack.WatchStateLoggerPlugin()
],
};
if (unitTesting) {
config.module.rules.push(
{
test: /-page\.js$/,
use: "@nativescript/webpack/helpers/script-hot-loader"
},
{
test: /\.(html|xml)$/,
use: "@nativescript/webpack/helpers/markup-hot-loader"
},
{ test: /\.(html|xml)$/, use: "@nativescript/webpack/helpers/xml-namespace-loader" }
);
}
if (report) {
// Generate report files for bundles content
config.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: "static",
openAnalyzer: false,
generateStatsFile: true,
reportFilename: resolve(projectRoot, "report", `report.html`),
statsFilename: resolve(projectRoot, "report", `stats.json`),
}));
}
if (snapshot) {
config.plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({
chunk: "vendor",
requireModules: [
"@nativescript/core/bundle-entry-points",
],
projectRoot,
webpackConfig: config,
snapshotInDocker,
skipSnapshotTools,
useLibs
}));
}
if (hmr) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
return config;
};

View file

@ -1,4 +1,4 @@
# NativeScript-Vue Application
# EnRecipes
> A native application built with NativeScript-Vue

View file

@ -15,14 +15,14 @@
android {
defaultConfig {
versionCode 2
versionCode 1
versionName '1.0.0'
applicationId 'com.vishnuraghav.enrecipes'
minSdkVersion 21
generatedDensities = []
ndk {
abiFilters.clear()
abiFilters.addAll(['arm64-v8a','x86'])
abiFilters.addAll(['arm64-v8a'])
}
}
aaptOptions {

View file

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<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" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="android.permission.CAMERA" tools:node="remove" />
<!-- <uses-feature android:name="android.hardware.camera" /> -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<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.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">
<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">
<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="adjustResize">
<meta-data android:name="SET_THEME_ON_LAUNCH" android:resource="@style/AppTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ns_primary">
#fafafa
#f1f3f5
</color>
<color name="ns_primaryDark">
#ff5200

View file

@ -1,7 +1,7 @@
<?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">
<style name="LaunchScreenThemeBase" parent="Theme.MaterialComponents.NoActionBar">
<item name="toolbarStyle">
@style/NativeScriptToolbarStyle
</item>
@ -27,7 +27,10 @@
<style name="LaunchScreenTheme" parent="LaunchScreenThemeBase">
</style>
<!-- theme to use AFTER launch screen is loaded -->
<style name="AppThemeBase" parent="Theme.AppCompat.Light.NoActionBar">
<style name="AppThemeBase" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="searchViewStyle">
@style/SearchViewMy
</item>
<item name="toolbarStyle">
@style/NativeScriptToolbarStyle
</item>
@ -40,21 +43,42 @@
<item name="colorAccent">
@color/ns_accent
</item>
<item name="colorControlNormal">
#868e96
</item>
</style>
<style name="AppTheme" parent="AppThemeBase">
</style>
<!-- theme for action-bar -->
<style name="NativeScriptToolbarStyleBase" parent="Widget.AppCompat.Toolbar">
<style name="NativeScriptToolbarStyleBase" parent="Widget.MaterialComponents.Toolbar">
<item name="contentInsetStart">
0dp
</item>
<item name="contentInsetEnd">
0dp
</item>
<item name="android:divider">
#ff0000
</item>
<item name="android:background">
@color/ns_primary
</item>
<item name="theme">
@style/ThemeOverlay.AppCompat.ActionBar
@style/ThemeOverlay.MaterialComponents.ActionBar
</item>
<item name="popupTheme">
@style/ThemeOverlay.AppCompat
@style/ThemeOverlay.MaterialComponents
</item>
</style>
<style name="NativeScriptToolbarStyle" parent="NativeScriptToolbarStyleBase">
</style>
<!-- theme for searchview -->
<style name="SearchViewMy" parent="Widget.AppCompat.Light.SearchView">
<item name="searchHintIcon">
@null
</item>
<!-- <item name="closeIcon">
@null
</item> -->
</style>
</resources>

View file

@ -1,27 +1,24 @@
// NativeScript core theme
// @see https://docs.nativescript.org/ui/theme
@import "~@nativescript/theme/core";
// Override variables here
$grayD4: #212121;
$grayD3: #424242;
$grayD2: #616161;
$grayD1: #757575;
$gray: #9e9e9e;
$grayL1: #e0e0e0;
$grayL2: #eeeeee;
$grayL3: #f5f5f5;
$grayL4: #fafafa;
$gray1: #f1f3f5;
$gray2: #e9ecef;
$gray3: #dee2e6;
$gray4: #ced4da;
$gray5: #adb5bd; // rgb(173,181,189)
$gray6: #868e96;
$gray7: #495057;
$gray8: #343a40;
$gray9: #212529;
$orange: #ff5200;
$fabRipple: #ff864d;
$red: #c92a2a;
// Global SCSS styling
// @see https://docs.nativescript.org/ui/styling
// * {
// font-size: 16;
// }
Page {
Page,
.ns-modal {
font-family: "Orkney-Regular";
}
.orkm {
@ -36,26 +33,30 @@ Page {
font-size: 16;
}
}
.ns-light {
Page,
ActionBar,
SearchBar,
TabView,
ListPicker {
color: $grayD4;
background: $grayL4;
color: $gray9;
background: $gray1;
}
MDRipple,
MDButton {
ripple-color: rgba($gray6, 0.2);
}
TabView {
tab-background-color: $grayL4;
selected-tab-text-color: $grayD4;
tab-background-color: $gray1;
selected-tab-text-color: $gray9;
}
.hr {
border-color: $grayL2;
border-color: $gray3;
}
.sd,
.fieldLabel {
background: $grayL4;
background: $gray1;
}
.referenceItem,
.recipeText,
@ -63,36 +64,32 @@ Page {
.recipeItem {
background: white;
}
.option-highlight,
.selected-sd-item {
background: $grayL2;
}
.sd-item,
.sd-group-header,
.time .bx {
color: $grayD2;
color: $gray8;
}
.fab-button {
color: white;
background-color: $orange;
}
.option,
.icon-option {
.bx,
.option-info {
color: $grayD2;
}
.option .bx,
.option .info {
color: $gray7;
}
.imageHolder {
color: $gray;
background: $grayL2;
color: $gray4;
background: $gray3;
}
.count {
color: $grayL4;
background: $grayD4;
color: $gray1;
background: $gray9;
}
.instruction {
border-color: $grayD4;
border-color: $gray9;
}
MDProgress {
progress-background-color: $gray4;
}
MDFloatingActionButton {
color: white;
}
}
@ -102,56 +99,56 @@ Page {
SearchBar,
TabView,
ListPicker {
color: $grayL4;
background: $grayD4;
color: $gray1;
background: $gray9;
}
TabView {
tab-background-color: $grayD4;
selected-tab-text-color: $grayL4;
tab-background-color: $gray9;
selected-tab-text-color: $gray1;
}
MDRipple,
MDButton {
ripple-color: rgba($gray4, 0.1);
}
.hr {
border-color: #111;
}
.sd,
.fieldLabel {
background: $grayD4;
background: $gray9;
}
.referenceItem,
.recipeText,
.overviewItem,
.recipeItem,
.option-highlight {
background: $grayD3;
.recipeItem {
background: $gray8;
}
.sd-item,
.sd-group-header,
.time .bx {
color: $grayL2;
color: $gray3;
}
.selected-sd-item {
background: #111;
}
.fab-button {
color: #111;
background: $orange;
}
.option,
.icon-option {
.bx,
.option-info {
color: $gray;
}
.option .bx,
.option .info {
color: $gray5;
}
.imageHolder {
color: $gray;
color: $gray8;
background: #111;
}
.count {
color: $grayD4;
background: $grayL4;
color: $gray9;
background: $gray1;
}
.instruction {
border-color: $grayL4;
border-color: $gray1;
}
MDProgress {
progress-background-color: $gray6;
}
MDFloatingActionButton {
color: $gray9;
}
}
// -----------------------------
@ -165,48 +162,56 @@ TimePickerField {
padding: 14 14 13;
margin: 8 0 0 0;
border-radius: 4;
border-color: $gray;
placeholder-color: $gray;
border-color: $gray6;
placeholder-color: $gray6;
}
TextView {
line-height: 12;
}
SearchBar {
font-family: "Orkney-Regular";
font-size: 16;
text-field-hint-color: $gray6;
}
TabView {
tab-text-color: $gray;
tab-text-color: $gray6;
}
.inputField {
margin-top: 16;
margin-bottom: 14;
}
.fieldLabel {
font-size: 12;
margin-left: 8;
padding: 0 8;
margin-left: 12;
padding: 0 4;
}
// prettier-ignore
.progressContainer{
.progressContainer {
width: 100%;
}
// prettier-ignore
.text-btn,
.group-header,
.category,
ActivityIndicator,
Progress {
MDActivityIndicator {
color: $orange;
}
MDProgress {
progress-color: $orange;
}
// -----------------------------
// ActionBar
ActionBar {
margin: 0;
padding: 0;
padding: 0 8;
height: 64;
.bx {
padding: 16 16 16 4;
GridLayout {
padding: 0;
margin: 0;
}
MDButton.bx {
padding: 0;
margin: 0;
vertical-alignment: center;
}
.title {
padding-left: 8;
padding-left: 12;
text-align: left;
font-size: 18;
}
@ -218,7 +223,6 @@ ActionBar {
}
.sd-item {
border-radius: 4;
padding: 0 16;
height: 48;
vertical-alignment: center;
.bx {
@ -226,25 +230,50 @@ ActionBar {
}
&.selected-sd-item {
color: $orange;
background: rgba($orange, 0.1);
MDRipple {
ripple-color: transparent;
}
}
// prettier-ignore
Label {
padding: 0 16 0 0;
font-size: 16;
vertical-alignment: center;
&.bx{
padding: 0 0 0 16;
}
}
MDRipple {
padding: 0 16;
}
}
.sd-group-header {
width: 100%;
padding: 8 8 16;
padding: 8 0 8 8;
}
MDRipple {
border-radius: 4;
}
MDButton {
padding: 8;
min-width: 0;
min-height: 0;
&.bx {
padding: 0;
width: 48;
height: 48;
margin: 0 8 0 0;
border-radius: 99;
}
}
// -----------------------------
// HOME
.emptyState {
.emptyStateContainer {
width: 100%;
height: 100%;
}
.noResult {
.emptyState {
line-height: 8;
padding: 0 32;
text-align: center;
@ -253,7 +282,7 @@ ActionBar {
.icon {
font-size: 64;
text-align: center;
color: $gray;
color: $gray5;
margin-bottom: 16;
}
.logo {
@ -271,6 +300,9 @@ ActionBar {
vertical-alignment: center;
}
}
&.noResult {
margin-top: 32;
}
}
// -----------------------------
// Recipe Items
@ -313,7 +345,7 @@ RadListView {
}
.swipe-item {
margin: 0 8;
background: #c62828;
background: $red;
color: #fff;
height: 128;
border-radius: 4;
@ -324,17 +356,19 @@ RadListView {
padding: 8;
}
.main-container {
padding: 16 8 128;
padding: 8 8 128;
.option {
font-size: 16;
padding: 16;
.bx {
margin: 0 24 0 0;
margin: 16 24 16 16;
}
.option-info {
.info {
font-size: 12;
line-height: 4;
}
StackLayout {
margin: 16 24 16 0;
}
}
}
// -----------------------------
@ -374,17 +408,16 @@ RadListView {
margin-top: 12;
.overviewItem {
border-radius: 4;
padding: 8;
margin: 8;
android-elevation: 1;
.bx {
color: $gray;
width: 24;
padding: 12 0 0 12;
color: $gray6;
horizontal-alignment: left;
}
.itemCount {
font-size: 16;
padding: 8 4 4;
padding: 8 12 12;
}
}
}
@ -394,7 +427,7 @@ RadListView {
padding-bottom: 12;
}
.ingredient-check {
margin-bottom: 12;
padding-bottom: 0;
}
.count {
width: 24;
@ -432,15 +465,14 @@ RadListView {
border-width: 0;
}
.referenceItem {
padding: 14 16;
margin: 8 16 8;
margin: 8 16;
border-radius: 4;
font-size: 16;
.bx {
font-size: 24;
}
.recipeLink {
padding: 0 16 0 0;
padding: 16;
margin: 0;
}
}
@ -448,7 +480,7 @@ RadListView {
font-size: 16;
line-height: 6;
padding: 16;
margin: 8 16 8;
margin: 8 16;
border-radius: 4;
}
}
@ -458,14 +490,10 @@ RadListView {
width: 100%;
height: 100%;
}
.fab-button {
height: 56;
width: 56;
border-radius: 28;
padding: 16;
MDFloatingActionButton {
margin: 16;
vertical-alignment: center;
android-elevation: 6;
background: $orange;
ripple-color: $fabRipple;
}
// -----------------------------
// EDIT RECIPE
@ -475,51 +503,67 @@ RadListView {
.text-btn {
font-size: 14;
horizontal-alignment: left;
padding: 16;
padding: 12;
margin: 8 0 0 0;
min-width: 0;
}
.closeBtn {
MDButton.closeBtn {
padding: 4;
margin-top: 16;
min-width: 0;
vertical-alignment: top;
}
// -----------------------------
// DIALOGS
.dialogContainer {
width: 100%;
color: $grayD4;
background: $grayL4;
color: $gray9;
background: $gray1;
font-size: 16;
&.dark {
color: $grayL4;
background: $grayD4;
color: $gray1;
background: $gray9;
}
.dialogTitle {
padding: 24 24 12;
padding: 24 24 16;
font-size: 20;
}
.dialogInput {
padding: 0 24 16;
}
.dialogDescription {
line-height: 4;
padding: 0 24 16;
line-height: 6;
padding: 0 24 8;
}
.actionItem {
padding: 8 24;
letter-spacing: 0;
text-transform: none;
padding: 16 24;
margin: 0;
}
.actionsContainer {
padding: 24;
padding: 8;
}
.action {
font-size: 12;
padding: 12;
min-width: 0;
color: #ff7043;
}
MDButton.actionIcon {
width: auto;
height: auto;
min-width: 0;
padding: 16 24;
border-radius: 4;
letter-spacing: 0;
text-transform: none;
margin: 0 16 16;
}
}
// -----------------------------
ActivityIndicator {
MDActivityIndicator {
width: 24;
height: 24;
margin: 16;

View file

@ -1,8 +1,9 @@
<template>
<Page @loaded="initializePage">
<Page @loaded="onPageLoad">
<ActionBar :flat="viewIsScrolled ? false : true">
<GridLayout rows="*" columns="auto, *">
<Label
<MDButton
variant="text"
class="bx"
:text="icon.menu"
automationText="Back"
@ -21,67 +22,73 @@
>
<Image src="res://logo_light" class="appIcon" stretch="aspectFit" />
</StackLayout>
<StackLayout orientation="horizontal" class="option">
<Label class="bx" :text="icon.info" />
<StackLayout>
<StackLayout class="m-8"></StackLayout>
<GridLayout columns="auto, *" class="option">
<Label col="0" class="bx" :text="icon.info" />
<StackLayout col="1">
<Label text="Version" />
<Label :text="getVersion" class="option-info" textWrap="true" />
<Label :text="getVersion" class="info" textWrap="true" />
</StackLayout>
</StackLayout>
<StackLayout
orientation="horizontal"
class="option"
@tap="openURL($event, 'https://github.com/vishnuraghavb/enrecipes')"
>
<Label class="bx" :text="icon.link" />
<Label text="View project on GitHub" />
</StackLayout>
<StackLayout
orientation="horizontal"
class="option"
@tap="openURL($event, 'https://t.me/enrecipes')"
>
<Label class="bx" :text="icon.telegram" />
<Label text="Join the Telegram group" />
</StackLayout>
</GridLayout>
<GridLayout columns="auto, *" class="option">
<MDRipple
colSpan="2"
@tap="openURL('https://github.com/vishnuraghavb/enrecipes')"
/>
<Label col="0" class="bx" :text="icon.link" />
<Label
verticalAlignment="center"
col="1"
text="View project on GitHub"
/>
</GridLayout>
<GridLayout columns="auto, *" class="option">
<MDRipple colSpan="2" @tap="openURL('https://t.me/enrecipes')" />
<Label col="0" class="bx" :text="icon.telegram" />
<Label
verticalAlignment="center"
col="1"
text="Join the Telegram group"
/>
</GridLayout>
<StackLayout class="hr m-10"></StackLayout>
<Label text="Author" class="group-header" />
<StackLayout
orientation="horizontal"
class="option"
@tap="openURL($event, 'https://www.vishnuraghav.com')"
>
<Label class="bx" :text="icon.user" />
<Label text="Vishnu Raghav" />
</StackLayout>
<StackLayout
orientation="horizontal"
class="option"
@tap="openURL($event, 'https://github.com/vishnuraghavb')"
>
<Label class="bx" :text="icon.link" />
<Label text="Follow on GitHub" />
</StackLayout>
<StackLayout
orientation="horizontal"
class="option"
@tap="openURL($event, 'https://mastodon.social/@vishnuraghavb')"
>
<Label class="bx" :text="icon.link" />
<Label text="Follow on Mastodon" />
</StackLayout>
<Label text="Author" class="group-header orkm" />
<GridLayout columns="auto, *" class="option">
<MDRipple
colSpan="2"
@tap="openURL('https://www.vishnuraghav.com')"
/>
<Label col="0" class="bx" :text="icon.user" />
<Label verticalAlignment="center" col="1" text="Vishnu Raghav" />
</GridLayout>
<GridLayout columns="auto, *" class="option">
<MDRipple
colSpan="2"
@tap="openURL('https://github.com/vishnuraghavb')"
/>
<Label col="0" class="bx" :text="icon.link" />
<Label verticalAlignment="center" col="1" text="Follow on GitHub" />
</GridLayout>
<GridLayout columns="auto, *" class="option">
<MDRipple
colSpan="2"
@tap="openURL('https://mastodon.social/@vishnuraghavb')"
/>
<Label col="0" class="bx" :text="icon.link" />
<Label verticalAlignment="center" col="1" text="Follow on Mastodon" />
</GridLayout>
</StackLayout>
</ScrollView>
</Page>
</template>
<script>
import { Utils, Application } from "@nativescript/core"
import { mapState, mapActions } from "vuex"
import { Application, Utils } from "@nativescript/core"
import { mapActions, mapState } from "vuex"
export default {
props: ["highlight", "showDrawer", "title"],
props: ["showDrawer", "title"],
computed: {
...mapState(["icon", "currentComponent"]),
getVersion() {
@ -98,16 +105,16 @@ export default {
},
methods: {
...mapActions(["setCurrentComponentAction"]),
initializePage() {
onPageLoad() {
this.setCurrentComponentAction("About")
},
// HELPERS
onScroll(args) {
args.scrollY
? (this.viewIsScrolled = true)
: (this.viewIsScrolled = false)
},
openURL(args, url) {
this.highlight(args)
openURL(url) {
Utils.openUrl(url)
},
},

View file

@ -1,8 +1,8 @@
<template>
<Page
@loaded="initialize"
@loaded="onPageLoad"
actionBarHidden="true"
:androidStatusBarBackground="themeName == 'Light' ? '#fafafa' : '#212121'"
:androidStatusBarBackground="appTheme == 'Light' ? '#f1f3f5' : '#212529'"
>
<RadSideDrawer
ref="drawer"
@ -19,12 +19,16 @@
columns="auto, 24, *"
v-for="(item, index) in topmenu"
:key="index"
@tap="navigateTo(item.component, false, false)"
class="sd-item orkm"
:class="{
'selected-sd-item': currentComponent === item.component,
}"
>
<MDRipple
row="0"
colSpan="3"
@tap="navigateTo(item.component, false, false)"
/>
<Label col="0" row="0" class="bx" :text="icon[item.icon]" />
<Label col="2" row="0" :text="item.title" />
</GridLayout>
@ -35,17 +39,17 @@
columns="*, auto"
v-if="categoriesWithRecipes.length"
>
<Label col="0" text="Categories" />
<Label
<Label verticalAlignment="center" col="0" text="Categories" />
<MDButton
variant="text"
@tap="toggleCatEdit"
col="2"
:text="catEditMode ? 'DONE' : 'RENAME'"
:text="editCategory ? 'DONE' : 'RENAME'"
/>
</GridLayout>
<ScrollView height="100%">
<StackLayout>
<GridLayout
@tap="navigateTo(item, false, true)"
v-for="(item, index) in categoriesWithRecipes"
:key="index"
class="sd-item orkm"
@ -54,6 +58,11 @@
}"
columns="auto, *, auto"
>
<MDRipple
row="0"
colSpan="3"
@tap="navigateTo(item, false, true)"
/>
<Label
col="0"
class="bx"
@ -61,8 +70,9 @@
margin="0 24 0 0"
/>
<Label col="1" :text="item" />
<Label
v-if="catEditMode"
<MDButton
variant="text"
v-if="editCategory"
@tap="renameCategory(item)"
col="2"
class="bx"
@ -74,30 +84,25 @@
</StackLayout>
<StackLayout row="1">
<StackLayout class="hr m-10"></StackLayout>
<!-- <StackLayout
orientation="horizontal"
class="sd-item orkm"
@tap="donate"
>
<Label class="bx" :text="icon.donate" margin="0 24 0 0" />
<Label text="Donate" />
</StackLayout> -->
<StackLayout
@tap="navigateTo(item.component, true, false)"
v-for="(item, index) in bottommenu"
:key="index"
orientation="horizontal"
<GridLayout
class="sd-item orkm"
:class="{
'selected-sd-item': currentComponent == item.title,
}"
v-for="(item, index) in bottommenu"
:key="index"
rows="48"
columns="auto, 24, *"
>
<Label class="bx" :text="icon[item.icon]" margin="0 24 0 0" />
<Label :text="item.title" />
</StackLayout>
<MDRipple
colSpan="3"
@tap="navigateTo(item.component, true, false)"
/>
<Label class="bx" col="0" :text="icon[item.icon]" />
<Label col="2" :text="item.title" />
</GridLayout>
</StackLayout>
</GridLayout>
<GridLayout ~mainContent rows="*" columns="*">
<Frame row="0" col="0" ref="mainFrame" id="main-frame">
<!-- Home -->
@ -119,29 +124,28 @@
<script>
import {
Utils,
ApplicationSettings,
AndroidApplication,
ApplicationEventData,
Application,
Color,
Utils,
} from "@nativescript/core"
import Theme from "@nativescript/theme"
import * as Toast from "nativescript-toast"
import * as application from "tns-core-modules/application"
import { mapActions, mapState } from "vuex"
import EnRecipes from "./EnRecipes.vue"
import Settings from "./Settings.vue"
import About from "./About.vue"
import PromptDialog from "./modal/PromptDialog.vue"
import { mapState, mapActions } from "vuex"
let page
import PromptDialog from "./modal/PromptDialog.vue"
export default {
components: {
EnRecipes,
Settings,
About,
},
data() {
return {
selectedCategory: null,
@ -176,8 +180,8 @@ export default {
icon: "info",
},
],
catEditMode: false,
themeName: "Light",
editCategory: false,
appTheme: "Light",
}
},
computed: {
@ -203,16 +207,19 @@ export default {
"initializeYieldUnits",
"renameCategoryAction",
]),
initialize() {
if (this.themeName === "Light") {
onPageLoad() {
if (this.appTheme === "Light") {
const View = android.view.View
const window = Application.android.startActivity.getWindow()
const decorView = window.getDecorView()
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
// window.setNavigationBarColor(new Color("#e0e0e0").android)
}
},
// HELPERS
toggleCatEdit() {
this.catEditMode = !this.catEditMode
this.editCategory = !this.editCategory
if (this.selectedCategory) this.setComponent("EnRecipes")
this.filterFavorites = this.filterTrylater = false
this.selectedCategory = null
@ -226,99 +233,22 @@ export default {
this.$showModal(PromptDialog, {
props: {
title: `Rename category`,
hint: category,
text: category,
action: "RENAME",
},
}).then((newCategory) => {
this.hijackGlobalBackEvent()
if (newCategory.length) {
this.renameCategoryAction({ current: category, updated: newCategory })
this.catEditMode = false
this.editCategory = false
this.navigateTo(newCategory, false, true)
}
})
},
highlight(args) {
let temp = args.object.className
args.object.className = `${temp} option-highlight`
setTimeout(() => (args.object.className = temp), 100)
},
// Navigation
setSelectedCategory(e) {
this.selectedCategory = e.item
this.closeDrawer()
},
hijackGlobalBackEvent() {
AndroidApplication.on(
AndroidApplication.activityBackPressedEvent,
this.globalBackEvent
)
},
releaseGlobalBackEvent() {
AndroidApplication.off(
AndroidApplication.activityBackPressedEvent,
this.globalBackEvent
)
},
globalBackEvent(args) {
function preventDefault() {
args.cancel = true
}
let vm = this
function isFiltered() {
vm.filterFavorites
? vm.setComponent("Favorites")
: vm.filterTrylater
? vm.setComponent("Try later")
: vm.selectedCategory
? vm.setComponent(vm.selectedCategory)
: vm.setComponent("EnRecipes")
}
if (this.$refs.drawer && this.$refs.drawer.nativeView.getIsOpen()) {
preventDefault()
this.closeDrawer()
this.catEditMode = false
} else if (
["Favorites", "Try later", this.selectedCategory].includes(
this.currentComponent
)
) {
preventDefault()
this.setComponent("EnRecipes")
this.filterFavorites = this.filterTrylater = false
this.selectedCategory = null
this.$refs.enrecipes.updateFilter()
this.releaseGlobalBackEvent()
}
},
navigateTo(to, isTrueComponent, isCategory) {
if (isTrueComponent) {
this.$navigateTo(to, {
frame: "main-frame",
props: {
highlight: this.highlight,
showDrawer: this.showDrawer,
restartApp: this.restartApp,
hijackGlobalBackEvent: this.hijackGlobalBackEvent,
releaseGlobalBackEvent: this.releaseGlobalBackEvent,
openAppSettingsPage: this.openAppSettingsPage,
},
backstackVisible: false,
})
this.closeDrawer()
} else if (!this.catEditMode || !isCategory) {
this.releaseGlobalBackEvent()
this.hijackGlobalBackEvent()
this.setComponent(to)
this.$navigateBack({ frame: "main-frame", backstackVisible: false })
this.filterFavorites = to === "Favorites" ? true : false
this.filterTrylater = to === "Try later" ? true : false
this.selectedCategory = isCategory ? to : null
this.$refs.enrecipes.updateFilter()
this.closeDrawer()
}
this.catEditMode = false
},
restartApp() {
// Code from nativescript-master-technology
const mStartActivity = new android.content.Intent(
@ -360,19 +290,91 @@ export default {
closeDrawer() {
this.$refs.drawer.nativeView.closeDrawer()
},
donate(args) {
this.highlight(args)
// NAVIGATION HANDLERS
hijackGlobalBackEvent() {
AndroidApplication.on(
AndroidApplication.activityBackPressedEvent,
this.globalBackEvent
)
},
releaseGlobalBackEvent() {
AndroidApplication.off(
AndroidApplication.activityBackPressedEvent,
this.globalBackEvent
)
},
globalBackEvent(args) {
function preventDefault() {
args.cancel = true
}
let vm = this
function isFiltered() {
vm.filterFavorites
? vm.setComponent("Favorites")
: vm.filterTrylater
? vm.setComponent("Try later")
: vm.selectedCategory
? vm.setComponent(vm.selectedCategory)
: vm.setComponent("EnRecipes")
}
if (this.$refs.drawer && this.$refs.drawer.nativeView.getIsOpen()) {
preventDefault()
this.closeDrawer()
this.editCategory = false
} else if (
["Favorites", "Try later", this.selectedCategory].includes(
this.currentComponent
)
) {
preventDefault()
this.setComponent("EnRecipes")
this.filterFavorites = this.filterTrylater = false
this.selectedCategory = null
this.$refs.enrecipes.updateFilter()
this.releaseGlobalBackEvent()
}
},
navigateTo(to, isTrueComponent, isCategory) {
if (isTrueComponent) {
this.$navigateTo(to, {
frame: "main-frame",
props: {
showDrawer: this.showDrawer,
restartApp: this.restartApp,
hijackGlobalBackEvent: this.hijackGlobalBackEvent,
releaseGlobalBackEvent: this.releaseGlobalBackEvent,
openAppSettingsPage: this.openAppSettingsPage,
},
backstackVisible: false,
})
this.closeDrawer()
} else if (!this.editCategory || !isCategory) {
this.releaseGlobalBackEvent()
this.hijackGlobalBackEvent()
this.setComponent(to)
this.$navigateBack({ frame: "main-frame", backstackVisible: false })
this.filterFavorites = to === "Favorites" ? true : false
this.filterTrylater = to === "Try later" ? true : false
this.selectedCategory = isCategory ? to : null
this.$refs.enrecipes.updateFilter()
this.closeDrawer()
}
this.editCategory = false
},
donate() {
Utils.openUrl("https://www.vishnuraghav.com/donate/")
},
},
created() {
this.themeName = ApplicationSettings.getString("appTheme", "Light")
this.appTheme = ApplicationSettings.getString("appTheme", "Light")
setTimeout((e) => {
Theme.setMode(Theme[this.themeName])
Theme.setMode(Theme[this.appTheme])
}, 50)
if (!this.recipes.length) this.initializeRecipes()
if (!this.categories.length) this.initializeCategories()
if (!this.yieldUnits.length) this.initializeYieldUnits()
this.initializeRecipes()
this.initializeCategories()
this.initializeYieldUnits()
},
}
</script>

View file

@ -1,8 +1,9 @@
<template>
<Page @loaded="initialize" @unloaded="releaseBackEvent">
<Page @loaded="onPageLoad" @unloaded="releaseBackEvent">
<ActionBar :flat="viewIsScrolled ? false : true">
<GridLayout rows="*" columns="auto, *, auto">
<Label
<MDButton
variant="text"
class="bx"
:text="icon.back"
automationText="Back"
@ -10,14 +11,15 @@
@tap="navigateBack"
/>
<Label class="title orkm" :text="title" col="1" />
<Label
v-if="hasEnoughDetails && !imageLoading"
<MDButton
variant="text"
v-if="hasChanges && !imageLoading"
class="bx"
:text="icon.save"
col="2"
@tap="saveOperation"
/>
<ActivityIndicator col="2" v-if="imageLoading" :busy="imageLoading" />
<MDActivityIndicator col="2" v-if="imageLoading" :busy="imageLoading" />
</GridLayout>
</ActionBar>
<GridLayout rows="*" columns="*">
@ -51,18 +53,16 @@
:text="icon.image"
/>
</StackLayout>
<StackLayout width="100%" :top="screenWidth - 42">
<transition :name="recipeContent.imageSrc ? 'null' : 'bounce'">
<Label
v-if="showFab"
horizontalAlignment="right"
@tap="imageHandler"
class="bx fab-button"
:text="icon.camera"
androidElevation="6"
/>
</transition>
</StackLayout>
<transition :name="recipeContent.imageSrc ? 'null' : 'bounce'">
<MDFloatingActionButton
v-if="showFab"
:top="screenWidth - 44"
:left="screenWidth - 88"
class="bx"
src="res://camera"
@tap="imageHandler"
/>
</transition>
</AbsoluteLayout>
<StackLayout margin="0 16">
@ -71,6 +71,7 @@
hint="My Healthy Recipe"
v-model="recipeContent.title"
autocapitalizationType="words"
autocorrect="true"
/>
<Label top="0" class="fieldLabel" text="Title" />
</AbsoluteLayout>
@ -103,7 +104,7 @@
<GridLayout columns="*, 8, *">
<AbsoluteLayout class="inputField" col="0">
<TextField
:text="formattedTimeRequired"
:text="timeRequired"
editable="false"
@tap="setTimeRequired"
/>
@ -127,30 +128,33 @@
v-model="recipeContent.ingredients[index].item"
:hint="`Item ${index + 1}`"
autocapitalizationType="words"
autocorrect="true"
/>
<TextField
width="72"
width="68"
col="2"
v-model="recipeContent.ingredients[index].quantity"
hint="1.000"
hint="1.00"
keyboardType="number"
/>
<TextField
width="64"
width="68"
col="4"
v-model="recipeContent.ingredients[index].unit"
hint="Unit"
editable="false"
@tap="showUnits($event)"
/>
<Label
<MDButton
variant="text"
col="6"
class="bx closeBtn"
:text="icon.close"
@tap="removeIngredient(index)"
/>
</GridLayout>
<Label
<MDButton
variant="text"
class="text-btn orkm"
text="+ ADD INGREDIENT"
@tap="addIngredient()"
@ -172,15 +176,18 @@
:hint="`Step ${index + 1}`"
v-model="recipeContent.instructions[index]"
editable="true"
autocorrect="true"
/>
<Label
<MDButton
variant="text"
col="2"
class="bx closeBtn"
:text="icon.close"
@tap="removeInstruction(index)"
/>
</GridLayout>
<Label
<MDButton
variant="text"
class="text-btn orkm"
text="+ ADD STEP"
@tap="addInstruction()"
@ -201,15 +208,22 @@
v-model="recipeContent.notes[index]"
:hint="`Note ${index + 1}`"
editable="true"
autocorrect="true"
/>
<Label
<MDButton
variant="text"
col="2"
class="bx closeBtn"
:text="icon.close"
@tap="removeNote(index)"
/>
</GridLayout>
<Label class="text-btn orkm" text="+ ADD NOTE" @tap="addNote()" />
<MDButton
variant="text"
class="text-btn orkm"
text="+ ADD NOTE"
@tap="addNote()"
/>
<StackLayout class="hr" margin="24 16"></StackLayout>
</StackLayout>
@ -226,15 +240,18 @@
v-model="recipeContent.references[index]"
hint="Text or Website/Video URL"
editable="true"
autocorrect="true"
/>
<Label
<MDButton
variant="text"
col="2"
class="bx closeBtn"
:text="icon.close"
@tap="removeReference(index)"
/>
</GridLayout>
<Label
<MDButton
variant="text"
class="text-btn orkm"
text="+ ADD REFERENCE"
@tap="addReference()"
@ -248,31 +265,30 @@
</template>
<script>
import { WorkerService } from "../worker.service"
const workerService = new WorkerService()
// import { WorkerService } from "../worker.service"
// const workerService = new WorkerService()
import {
Screen,
AndroidApplication,
ImageSource,
path,
getFileAccess,
knownFolders,
Utils,
File,
Application,
ApplicationSettings,
File,
getFileAccess,
ImageSource,
knownFolders,
path,
Screen,
Utils,
} from "@nativescript/core"
import { Mediafilepicker } from "nativescript-mediafilepicker"
import * as Permissions from "@nativescript-community/perms"
import * as Toast from "nativescript-toast"
import * as ImagePicker from "@nativescript/imagepicker"
import { ImageCropper } from "nativescript-imagecropper"
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 * as Permissions from "@nativescript-community/perms"
import * as Toast from "nativescript-toast"
export default {
props: [
@ -310,6 +326,7 @@ export default {
newRecipeID: null,
showFab: false,
imageLoading: false,
cacheImagePath: null,
}
},
computed: {
@ -324,13 +341,16 @@ export default {
screenWidth() {
return Screen.mainScreen.widthDIPs
},
hasEnoughDetails() {
appTheme() {
return Application.systemAppearance()
},
hasChanges() {
return (
JSON.stringify(this.recipeContent) !==
JSON.stringify(this.tempRecipeContent)
)
},
formattedTimeRequired() {
timeRequired() {
let t = this.recipeContent.timeRequired.split(":")
let h = parseInt(t[0])
let m = parseInt(t[1])
@ -345,7 +365,7 @@ export default {
"addCategoryAction",
"addYieldUnitAction",
]),
initialize() {
onPageLoad() {
this.showFab = true
},
@ -394,11 +414,11 @@ export default {
props: {
title: "Category",
list: [...this.categories],
height: "408",
action: "CREATE NEW",
// height: "420",
action: "ADD NEW",
},
}).then((action) => {
if (action == "CREATE NEW") {
if (action == "ADD NEW") {
this.$showModal(PromptDialog, {
props: {
title: "New category",
@ -425,11 +445,11 @@ export default {
props: {
title: "Yield measured in",
list: [...this.yieldUnits],
height: "408",
action: "CREATE NEW",
// height: "420",
action: "ADD NEW",
},
}).then((action) => {
if (action == "CREATE NEW") {
if (action == "ADD NEW") {
this.$showModal(PromptDialog, {
props: {
title: "New yield unit",
@ -456,7 +476,7 @@ export default {
props: {
title: "Unit",
list: [...this.units],
height: "408",
// height: "420",
},
}).then((action) => {
this.hijackBackEvent()
@ -466,21 +486,19 @@ export default {
// NAVIGATION HANDLERS
navigateBack() {
if (this.hasEnoughDetails) {
if (this.hasChanges) {
this.blockModal = true
this.$showModal(ConfirmDialog, {
props: {
title: "Unsaved changes",
description:
"Do you want to save the changes you made in this recipe?",
"Are you sure you want to discard unsaved changes to this recipe?",
cancelButtonText: "DISCARD",
okButtonText: "SAVE",
okButtonText: "KEEP EDITING",
},
}).then((action) => {
this.blockModal = false
if (action) {
this.saveOperation()
} else if (action != null) {
if (action != null && !action) {
this.$navigateBack()
this.releaseBackEvent()
}
@ -503,7 +521,7 @@ export default {
)
},
backEvent(args) {
if (this.hasEnoughDetails && !this.blockModal) {
if (this.hasChanges && !this.blockModal) {
args.cancel = true
this.navigateBack()
}
@ -522,28 +540,22 @@ export default {
}).then((action) => {
this.blockModal = false
if (action) {
this.permissionCheck(
this.imagePickerPermissionConfirmation,
this.imagePicker
)
this.permissionCheck(this.permissionConfirmation, this.imagePicker)
} else if (action != null) {
this.recipeContent.imageSrc = null
this.releaseBackEvent()
}
})
} else {
this.permissionCheck(
this.imagePickerPermissionConfirmation,
this.imagePicker
)
this.permissionCheck(this.permissionConfirmation, this.imagePicker)
}
},
imagePickerPermissionConfirmation() {
permissionConfirmation() {
return this.$showModal(ConfirmDialog, {
props: {
title: "Grant permission",
description:
"EnRecipes requires storage and camera permission in order to set recipe photo.",
"EnRecipes requires storage permission in order to set recipe photo.",
cancelButtonText: "NOT NOW",
okButtonText: "CONTINUE",
},
@ -553,52 +565,82 @@ export default {
if (!ApplicationSettings.getBoolean("storagePermissionAsked", false)) {
confirmation().then((e) => {
if (e) {
Permissions.request("camera").then((res) => {
let status = res[Object.keys(res)[0]]
if (status === "authorized") action()
if (status === "never_ask_again")
ApplicationSettings.setBoolean("storagePermissionAsked", true)
if (status === "denied")
Toast.makeText("Permission denied").show()
Permissions.request("photo").then((status) => {
switch (status[0]) {
case "authorized":
action()
break
case "never_ask_again":
ApplicationSettings.setBoolean("storagePermissionAsked", true)
break
case "denied":
Toast.makeText("Permission denied").show()
break
default:
break
}
})
}
})
} else {
Permissions.check("camera").then((res) => {
if (res[0] !== "authorized") {
confirmation().then((e) => {
e && this.openAppSettingsPage()
})
} else {
Permissions.request("storage").then((res) => {
let status = res[Object.keys(res)[0]]
if (status !== "authorized") {
confirmation().then((e) => {
e && this.openAppSettingsPage()
})
} else action()
})
}
Permissions.check("photo").then((res) => {
res[0] !== "authorized"
? confirmation().then((e) => e && this.openAppSettingsPage())
: action()
})
}
},
imagePicker() {
const vm = this
const mediafilepicker = new Mediafilepicker()
mediafilepicker.openImagePicker({
android: {
isCaptureMood: false, // if true then camera will open directly.
isNeedCamera: true,
maxNumberFiles: 1,
isNeedFolderList: true,
},
})
mediafilepicker.on("getFiles", (image) => {
ApplicationSettings.setBoolean("storagePermissionAsked", true)
vm.recipeContent.imageSrc = image.object.get("results")[0].file
ApplicationSettings.setBoolean("storagePermissionAsked", true)
this.cacheImagePath = path.join(
knownFolders.temp().path,
`${this.getRandomID()}.jpg`
)
let screenWidth = Math.round(this.screenWidth * 2)
ImagePicker.create({
mode: "single",
mediaType: ImagePicker.ImagePickerMediaType.Image,
})
.present()
.then((selection) => {
let imgPath = selection[0]._android
ImageSource.fromFile(imgPath).then((image) => {
ImageCropper.prototype
.show(
image,
{
width: screenWidth,
height: screenWidth,
},
{
hideBottomControls: true,
toolbarTitle: "Crop photo",
statusBarColor: "#ff5200",
toolbarTextColor:
this.appTheme == "light" ? "#212529" : "#f1f3f5",
toolbarColor:
this.appTheme == "light" ? "#f1f3f5" : "#212529",
cropFrameColor: "#ff5200",
}
)
.then((cropped) => {
cropped.image.saveToFile(this.cacheImagePath, "jpg", 75)
this.recipeContent.imageSrc = this.cacheImagePath
})
})
})
},
// INPUT FIELD HANDLERS
fieldDeletionConfirm(item) {
return this.$showModal(ConfirmDialog, {
props: {
title: `Delete ${item}?`,
cancelButtonText: "CANCEL",
okButtonText: "DELETE",
},
})
},
addIngredient() {
this.recipeContent.ingredients.push({
item: "",
@ -607,30 +649,55 @@ export default {
})
},
removeIngredient(index) {
this.recipeContent.ingredients.splice(index, 1)
if (this.recipeContent.ingredients[index].item.length) {
this.fieldDeletionConfirm("ingredient").then((res) => {
if (res) {
this.recipeContent.ingredients.splice(index, 1)
}
})
} else this.recipeContent.ingredients.splice(index, 1)
},
addInstruction() {
this.recipeContent.instructions.push("")
},
removeInstruction(index) {
this.recipeContent.instructions.splice(index, 1)
if (this.recipeContent.instructions[index].length) {
this.fieldDeletionConfirm("instruction").then((res) => {
if (res) {
this.recipeContent.instructions.splice(index, 1)
}
})
} else this.recipeContent.instructions.splice(index, 1)
},
addNote() {
this.recipeContent.notes.push("")
},
removeNote(index) {
this.recipeContent.notes.splice(index, 1)
if (this.recipeContent.notes[index].length) {
this.fieldDeletionConfirm("note").then((res) => {
if (res) {
this.recipeContent.notes.splice(index, 1)
}
})
} else this.recipeContent.notes.splice(index, 1)
},
addReference() {
this.recipeContent.references.push("")
},
removeReference(index) {
this.recipeContent.references.splice(index, 1)
if (this.recipeContent.references[index].length) {
this.fieldDeletionConfirm("reference").then((res) => {
if (res) {
this.recipeContent.references.splice(index, 1)
}
})
} else this.recipeContent.references.splice(index, 1)
},
// SAVE OPERATION
clearEmptyFields() {
if (!this.recipeContent.title)
this.recipeContent.title = "Untitled Recipe"
@ -651,37 +718,50 @@ export default {
this.imageLoading = true
this.clearEmptyFields()
this.recipeContent.lastModified = new Date()
if (this.cacheImagePath) {
let recipeImage = path.join(
knownFolders
.documents()
.getFolder("EnRecipes")
.getFolder("Images").path,
`${this.getRandomID()}.jpg`
)
let binarySource = File.fromPath(this.cacheImagePath).readSync()
File.fromPath(recipeImage).writeSync(binarySource)
this.recipeContent.imageSrc = recipeImage
knownFolders.temp().clear()
}
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()
if (
this.tempRecipeContent.imageSrc &&
this.tempRecipeContent.imageSrc !== this.recipeContent.imageSrc
) {
getFileAccess().deleteFile(this.tempRecipeContent.imageSrc)
}
} else if (this.tempRecipeContent.imageSrc) {
getFileAccess().deleteFile(this.tempRecipeContent.imageSrc)
this.saveRecipe()
} else this.saveRecipe()
},
imageSaveOperation() {
let imgSavedToPath = path.join(
knownFolders
.documents()
.getFolder("EnRecipes")
.getFolder("Images").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()
}
this.saveRecipe()
},
// imageSaveOperation() {
// let imgSavedToPath = path.join(
// knownFolders
// .documents()
// .getFolder("EnRecipes")
// .getFolder("Images").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({
@ -705,6 +785,7 @@ export default {
this.setCurrentComponentAction("EditRecipe")
}, 500)
this.title = this.recipeID ? "Edit recipe" : "New recipe"
if (this.recipeID) {
let recipe = this.recipes.filter((e) => e.id === this.recipeID)[0]
Object.assign(this.recipeContent, JSON.parse(JSON.stringify(recipe)))
@ -713,13 +794,14 @@ export default {
JSON.parse(JSON.stringify(this.recipeContent))
)
} else {
if (this.selectedCategory)
this.recipeContent.category = this.selectedCategory
if (this.filterFavorites) this.recipeContent.isFavorite = true
Object.assign(
this.tempRecipeContent,
JSON.parse(JSON.stringify(this.recipeContent))
)
if (this.selectedCategory)
this.recipeContent.category = this.selectedCategory
if (this.filterFavorites) this.recipeContent.isFavorite = true
this.newRecipeID = this.getRandomID()
}
this.hijackBackEvent()

View file

@ -1,5 +1,5 @@
<template>
<Page @loaded="initializePage">
<Page @loaded="onPageLoad">
<ActionBar :flat="viewIsScrolled ? false : true">
<!-- Search Actionbar -->
<GridLayout
@ -7,9 +7,10 @@
columns="auto, *"
verticalAlignment="center"
>
<Label
<MDButton
class="bx"
:text="icon.back"
variant="text"
automationText="Back"
col="0"
@tap="closeSearch"
@ -17,33 +18,35 @@
<SearchBar
col="1"
hint="Search"
textFieldHintColor="#bdbdbd"
v-model="searchQuery"
@textChange="updateFilter"
@clear="updateFilter"
@clear="clearSearch"
/>
</GridLayout>
<!-- Home Actionbar -->
<GridLayout v-else columns="auto, *, auto, auto">
<Label
<MDButton
class="bx"
col="0"
variant="text"
@tap="showDrawer"
:text="icon.menu"
automationText="Back"
@tap="showDrawer"
col="0"
/>
<Label class="title orkm" :text="currentComponent" col="1" />
<Label
<MDButton
v-if="recipes.length"
class="bx"
:text="icon.search"
variant="text"
col="2"
@tap="openSearch"
/>
<Label
<MDButton
v-if="recipes.length"
class="bx"
:text="icon.sort"
variant="text"
col="3"
@tap="sortDialog"
/>
@ -58,10 +61,10 @@
@itemSwipeProgressChanged="onSwiping"
@itemSwipeProgressEnded="onSwipeEnded"
@scrolled="onScroll"
@itemTap="viewRecipe"
:filteringFunction="filterFunction"
:sortingFunction="sortFunction"
>
<!-- @itemTap="viewRecipe" -->
<v-template>
<GridLayout
class="recipeItem"
@ -69,6 +72,7 @@
columns="112, *"
androidElevation="1"
>
<MDRipple colSpan="2" @tap="viewRecipe(recipe)" />
<GridLayout class="imageHolder card" rows="112" columns="112">
<Image
row="0"
@ -114,31 +118,85 @@
<StackLayout height="128"></StackLayout>
</v-template>
</RadListView>
<GridLayout rows="96, auto, *" columns="*" class="emptyState">
<GridLayout rows="*, auto, *, 88" columns="*" class="emptyStateContainer">
<StackLayout
col="0"
row="1"
class="noResult"
v-if="!recipes.length && !filterFavorites && !filterTrylater"
class="emptyState"
v-if="
!recipes.length &&
!filterFavorites &&
!filterTrylater &&
!selectedCategory
"
@tap="addRecipe"
>
<Image class="logo" src="res://icon_gray" stretch="aspectFit" />
<Label class="bx icon" :text="icon.plusCircle" />
<Label
class="title orkm"
text="Start adding your recipes!"
textWrap="true"
/>
<StackLayout orientation="horizontal" horizontalAlignment="center">
<Label text="Use the " textWrap="true" />
<Label text="Use the " />
<Label class="bx" :text="icon.plus" />
<Label text=" button to add a new recipe" textWrap="true" />
<Label text=" button to add one" />
</StackLayout>
</StackLayout>
<StackLayout
col="0"
row="1"
class="noResult"
class="emptyState"
v-if="!filteredRecipes.length && filterFavorites && !searchQuery"
>
<Label class="bx icon" :text="icon.heartOutline" textWrap="true" />
<Label class="title orkm" text="No favorites yet" textWrap="true" />
<Label
text="Recipes you mark as favorite will be listed here"
textWrap="true"
/>
</StackLayout>
<StackLayout
col="0"
row="1"
class="emptyState"
v-if="!filteredRecipes.length && filterTrylater && !searchQuery"
>
<Label class="bx icon" :text="icon.trylaterOutline" textWrap="true" />
<Label class="title orkm" text="All done!" textWrap="true" />
<Label
text="Recipes you mark as try later will be listed here"
textWrap="true"
/>
</StackLayout>
<StackLayout
col="0"
row="1"
class="emptyState"
v-if="
!filteredRecipes.length &&
!filterFavorites &&
!filterTrylater &&
selectedCategory
"
>
<Label class="bx icon" :text="icon.labelOutline" textWrap="true" />
<Label
class="title orkm"
text="Category looks empty"
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 recipe" textWrap="true" />
</StackLayout>
</StackLayout>
<StackLayout
col="0"
row="0"
class="emptyState noResult"
v-if="!filteredRecipes.length && searchQuery"
verticalAlignment="top"
>
<Label class="bx icon" :text="icon.search" textWrap="true" />
<Label class="title orkm" text="No recipes found" textWrap="true" />
@ -153,45 +211,15 @@
textWrap="true"
/>
</StackLayout>
<StackLayout
col="0"
row="1"
class="noResult"
v-if="!filteredRecipes.length && filterFavorites && !searchQuery"
>
<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"
/>
</StackLayout>
<StackLayout
col="0"
row="1"
class="noResult"
v-if="!filteredRecipes.length && filterTrylater && !searchQuery"
>
<Label class="bx icon" :text="icon.trylaterOutline" textWrap="true" />
<Label
class="title orkm"
text="Nothing to try next!"
textWrap="true"
/>
<Label
text="Recipes you wanted to try later will be listed here"
textWrap="true"
/>
</StackLayout>
</GridLayout>
<GridLayout id="btnFabContainer" rows="*,auto" columns="*,auto">
<GridLayout id="btnFabContainer" rows="*, auto" columns="*, auto">
<transition name="bounce">
<Label
<MDFloatingActionButton
v-if="showFAB"
row="1"
col="1"
class="bx fab-button"
:text="icon.plus"
src="res://plus"
@tap="addRecipe"
/>
</transition>
@ -201,13 +229,14 @@
</template>
<script>
import { Utils, AndroidApplication } from "@nativescript/core"
import { AndroidApplication, Utils } from "@nativescript/core"
import { mapActions, mapState } from "vuex"
import EditRecipe from "./EditRecipe.vue"
import ViewRecipe from "./ViewRecipe.vue"
import ActionDialog from "./modal/ActionDialog.vue"
import ConfirmDialog from "./modal/ConfirmDialog.vue"
import { mapState, mapActions } from "vuex"
export default {
props: [
@ -261,7 +290,7 @@ export default {
},
methods: {
...mapActions(["setCurrentComponentAction", "deleteRecipeAction"]),
initializePage() {
onPageLoad() {
this.filterFavorites
? this.setComponent("Favorites")
: this.filterTrylater
@ -271,10 +300,46 @@ export default {
: this.setComponent("EnRecipes")
this.showFAB = true
},
// HELPERS
openSearch() {
this.showSearch = true
this.showFAB = false
this.hijackLocalBackEvent()
},
closeSearch() {
if (this.searchQuery) this.updateFilter()
this.searchQuery = ""
Utils.ad.dismissSoftInput()
this.showSearch = false
this.showFAB = true
this.releaseLocalBackEvent()
},
setComponent(comp) {
this.setCurrentComponentAction(comp)
this.hijackGlobalBackEvent()
},
clearSearch() {
if (this.searchQuery !== "") {
this.updateFilter()
}
},
formattedTime(time) {
let t = time.split(":")
let h = parseInt(t[0])
let m = parseInt(t[1])
return {
time: h ? (m ? `${h}h ${m}m` : `${h}h`) : `${m}m`,
duration: `${h}${m}`,
}
},
onScroll(args) {
args.scrollOffset
? (this.viewIsScrolled = true)
: (this.viewIsScrolled = false)
},
// NAVIGATION HANDLERS
hijackLocalBackEvent() {
this.releaseGlobalBackEvent()
AndroidApplication.on(
@ -293,20 +358,49 @@ export default {
args.cancel = true
this.closeSearch()
},
closeSearch() {
if (this.searchQuery) this.updateFilter()
this.searchQuery = ""
Utils.ad.dismissSoftInput()
this.showSearch = false
this.releaseLocalBackEvent()
addRecipe() {
this.showFAB = false
this.releaseGlobalBackEvent()
this.$navigateTo(EditRecipe, {
// transition: {
// name: "fade",
// duration: 200,
// curve: "easeOut",
// },
props: {
selectedCategory: this.selectedCategory,
openAppSettingsPage: this.openAppSettingsPage,
filterFavorites: this.filterFavorites,
},
})
},
viewRecipe(item) {
let index = this.recipes.indexOf(item)
this.showFAB = false
this.$navigateTo(ViewRecipe, {
// transition: {
// name: "fade",
// duration: 200,
// curve: "easeOut",
// },
props: {
filterTrylater: this.filterTrylater,
recipeIndex: index,
recipeID: item.id,
hijackGlobalBackEvent: this.hijackGlobalBackEvent,
releaseGlobalBackEvent: this.releaseGlobalBackEvent,
},
})
},
// LIST HANDLERS
sortDialog() {
this.releaseGlobalBackEvent()
this.$showModal(ActionDialog, {
props: {
title: "Sort by",
list: ["Natural order", "Title", "Duration", "Last modified"],
height: "216", // 54*4
height: "225",
},
}).then((action) => {
if (action && action !== "Cancel" && this.sortType !== action) {
@ -346,10 +440,6 @@ export default {
break
}
},
setComponent(comp) {
this.setCurrentComponentAction(comp)
this.hijackGlobalBackEvent()
},
updateFilter() {
let listView = this.$refs.listView.nativeView
setTimeout((e) => {
@ -392,6 +482,8 @@ export default {
this.deleteRecipe(index, recipeID)
this.rightAction = false
},
// DATA HANDLERS
deleteRecipe(index, recipeID) {
this.deletionDialogActive = true
this.$showModal(ConfirmDialog, {
@ -408,54 +500,6 @@ export default {
this.deletionDialogActive = false
})
},
formattedTime(time) {
let t = time.split(":")
let h = parseInt(t[0])
let m = parseInt(t[1])
return {
time: h ? (m ? `${h}h ${m}m` : `${h}h`) : `${m}m`,
duration: `${h}${m}`,
}
},
onScroll(args) {
args.scrollOffset
? (this.viewIsScrolled = true)
: (this.viewIsScrolled = false)
},
addRecipe() {
this.showFAB = false
this.releaseGlobalBackEvent()
this.$navigateTo(EditRecipe, {
// transition: {
// name: "fade",
// duration: 200,
// curve: "easeOut",
// },
props: {
selectedCategory: this.selectedCategory,
openAppSettingsPage: this.openAppSettingsPage,
filterFavorites: this.filterFavorites,
},
})
},
viewRecipe({ item, index }) {
this.showFAB = false
this.$navigateTo(ViewRecipe, {
// transition: {
// name: "fade",
// duration: 200,
// curve: "easeOut",
// },
props: {
filterTrylater: this.filterTrylater,
recipeIndex: index,
recipeID: item.id,
hijackGlobalBackEvent: this.hijackGlobalBackEvent,
releaseGlobalBackEvent: this.releaseGlobalBackEvent,
},
})
},
},
mounted() {
this.showFAB = true

View file

@ -1,9 +1,10 @@
<template>
<Page @loaded="initializePage">
<Page @loaded="onPageLoad">
<ActionBar :flat="viewIsScrolled ? false : true">
<GridLayout rows="*" columns="auto, *">
<Label
class="bx"
<MDButton
class="bx left"
variant="text"
:text="icon.menu"
automationText="Back"
@tap="showDrawer"
@ -14,55 +15,59 @@
</ActionBar>
<ScrollView scrollBarIndicatorVisible="false" @scroll="onScroll">
<StackLayout class="main-container">
<Label text="Interface" class="group-header" />
<StackLayout
orientation="horizontal"
class="option"
@tap="selectThemes"
>
<Label verticalAlignment="center" class="bx" :text="icon.theme" />
<StackLayout>
<Label text="Interface" class="group-header orkm" />
<GridLayout columns="auto, *" class="option">
<MDRipple colSpan="2" @tap="selectThemes" />
<Label
col="0"
verticalAlignment="center"
class="bx"
:text="icon.theme"
/>
<StackLayout col="1">
<Label text="Theme" />
<Label :text="appTheme" class="option-info" textWrap="true" />
<Label :text="appTheme" class="info" textWrap="true" />
</StackLayout>
</StackLayout>
</GridLayout>
<StackLayout class="hr m-10"></StackLayout>
<Label text="Database" class="group-header" />
<StackLayout orientation="horizontal" class="option" @tap="backupCheck">
<Label class="bx" :text="icon.export" />
<StackLayout>
<Label text="Database" class="group-header orkm" />
<GridLayout columns="auto, *" class="option">
<MDRipple colSpan="2" @tap="exportCheck" />
<Label col="0" class="bx" :text="icon.export" />
<StackLayout col="1">
<Label text="Export a full backup" />
<GridLayout
class="progressContainer"
v-if="backupInProgress"
columns="*, 64"
>
<Progress col="0" :value="backupProgress" />
<MDProgress
col="0"
:value="backupProgress"
maxValue="100"
></MDProgress>
<Label col="1" :text="` ${backupProgress}%`" />
</GridLayout>
<Label
v-else
text="Generates a zip file that contains all your data. This file can be imported back."
class="option-info"
class="info"
textWrap="true"
/>
</StackLayout>
</StackLayout>
<StackLayout
orientation="horizontal"
class="option"
@tap="restoreCheck"
>
<Label class="bx" :text="icon.import" />
<StackLayout>
</GridLayout>
<GridLayout columns="auto, *" class="option"
><MDRipple colSpan="2" @tap="importCheck" />
<Label col="0" class="bx" :text="icon.import" />
<StackLayout col="1">
<Label text="Import from backup" />
<Label
text="Supports full backups exported by this app."
class="option-info"
class="info"
textWrap="true"
/>
</StackLayout>
</StackLayout>
</GridLayout>
</StackLayout>
</ScrollView>
</Page>
@ -83,14 +88,13 @@ import { Zip } from "@nativescript/zip"
import * as Toast from "nativescript-toast"
import * as Filepicker from "nativescript-plugin-filepicker"
import Theme from "@nativescript/theme"
import { mapState, mapActions } from "vuex"
import ActionDialog from "./modal/ActionDialog.vue"
import ConfirmDialog from "./modal/ConfirmDialog.vue"
import { Couchbase } from "nativescript-couchbase-plugin"
import { mapState, mapActions } from "vuex"
export default {
props: [
"highlight",
"showDrawer",
"restartApp",
"hijackGlobalBackEvent",
@ -121,22 +125,25 @@ export default {
"importYieldUnitsAction",
"importRecipesAction",
]),
initializePage() {
onPageLoad() {
this.setCurrentComponentAction("Settings")
this.releaseGlobalBackEvent()
},
// HELPERS
onScroll(args) {
args.scrollY
? (this.viewIsScrolled = true)
: (this.viewIsScrolled = false)
},
selectThemes(args) {
this.highlight(args)
// THEME SELECTION
selectThemes() {
this.$showModal(ActionDialog, {
props: {
title: "Theme",
list: ["Light", "Dark"],
height: "108",
height: "113",
},
}).then((action) => {
if (action && action !== "Cancel" && this.appTheme !== action) {
@ -159,53 +166,23 @@ export default {
})
},
writeFile(file, data) {
file.writeText(JSON.stringify(data))
},
BackupDataFiles(option) {
const folder = path.join(knownFolders.documents().path, "EnRecipes")
const EnRecipesFile = File.fromPath(path.join(folder, "EnRecipes.json"))
let userCategoriesFile, userYieldUnitsFile
if (this.userCategories.length) {
userCategoriesFile = File.fromPath(
path.join(folder, "userCategories.json")
// EXPORT HANDLERS
exportCheck() {
if (!this.recipes.length) {
Toast.makeText(
"Add at least one recipe to perform a backup",
"long"
).show()
} else {
this.permissionCheck(
this.permissionConfirmation,
"EnRecipes requires storage permission in order to backup your data to this device.",
this.exportBackup
)
}
if (this.userYieldUnits.length) {
userYieldUnitsFile = File.fromPath(
path.join(folder, "userYieldUnits.json")
)
}
switch (option) {
case "create":
this.writeFile(EnRecipesFile, this.recipes)
this.userCategories.length &&
this.writeFile(userCategoriesFile, this.userCategories)
this.userYieldUnits.length &&
this.writeFile(userYieldUnitsFile, this.userYieldUnits)
break
case "delete":
EnRecipesFile.remove()
this.userCategories.length && userCategoriesFile.remove()
this.userYieldUnits.length && userYieldUnitsFile.remove()
break
default:
break
}
},
backupPermissionConfirmation() {
return this.$showModal(ConfirmDialog, {
props: {
title: "Grant permission",
description:
"EnRecipes requires storage permission in order to backup your data to this device",
cancelButtonText: "NOT NOW",
okButtonText: "CONTINUE",
},
})
},
backupData() {
this.BackupDataFiles("create")
exportBackup() {
this.exportFiles("create")
let date = new Date()
let formattedDate =
date.getFullYear() +
@ -242,59 +219,70 @@ export default {
"Backup file successfully saved to Downloads",
"long"
).show()
this.BackupDataFiles("delete")
this.exportFiles("delete")
})
},
backupCheck(args) {
let btn = args.object
this.highlight(args)
if (!this.recipes.length) {
Toast.makeText(
"Add at least one recipe to perform a backup",
"long"
).show()
} else {
this.permissionCheck(this.backupPermissionConfirmation, this.backupData)
exportFiles(option) {
const folder = path.join(knownFolders.documents().path, "EnRecipes")
const EnRecipesFile = File.fromPath(path.join(folder, "EnRecipes.json"))
let userCategoriesFile, userYieldUnitsFile
if (this.userCategories.length) {
userCategoriesFile = File.fromPath(
path.join(folder, "userCategories.json")
)
}
if (this.userYieldUnits.length) {
userYieldUnitsFile = File.fromPath(
path.join(folder, "userYieldUnits.json")
)
}
switch (option) {
case "create":
this.writeDataToFile(EnRecipesFile, this.recipes)
this.userCategories.length &&
this.writeDataToFile(userCategoriesFile, this.userCategories)
this.userYieldUnits.length &&
this.writeDataToFile(userYieldUnitsFile, this.userYieldUnits)
break
case "delete":
EnRecipesFile.remove()
this.userCategories.length && userCategoriesFile.remove()
this.userYieldUnits.length && userYieldUnitsFile.remove()
break
default:
break
}
},
restorePermissionConfirmation() {
return this.$showModal(ConfirmDialog, {
props: {
title: "Grant permission",
description:
"EnRecipes requires storage permission in order to restore your data from a previous backup.",
cancelButtonText: "NOT NOW",
okButtonText: "CONTINUE",
},
})
writeDataToFile(file, data) {
file.writeText(JSON.stringify(data))
},
restoreCheck(args) {
let btn = args.object
this.highlight(args)
// IMPORT HANDLERS
importCheck() {
this.permissionCheck(
this.restorePermissionConfirmation,
this.permissionConfirmation,
"EnRecipes requires storage permission in order to restore your data from a previous backup.",
this.openFilePicker
)
},
openFilePicker() {
let context = Filepicker.create({
mode: "single", // use "multiple" for multiple selection
Filepicker.create({
mode: "single",
extensions: ["zip"],
})
context.present().then((selection) => {
Toast.makeText("Processing...").show()
let result = selection[0]
let zipPath = result
let dest = knownFolders.documents().path
this.validateZipContent(zipPath)
})
.present()
.then((selection) => {
Toast.makeText("Processing...").show()
let result = selection[0]
let zipPath = result
let dest = knownFolders.documents().path
this.validateZipContent(zipPath)
})
},
importDataToDB(data, db, zipPath) {
switch (db) {
case "EnRecipesDB":
this.copyImages(zipPath)
this.importImages(zipPath)
this.importRecipesAction(data)
break
case "userCategoriesDB":
@ -307,7 +295,7 @@ export default {
break
}
},
isImportedDataValid(file) {
isFileDataValid(file) {
file.forEach((file, i) => {
if (File.exists(file.path)) {
File.fromPath(file.path)
@ -329,7 +317,7 @@ export default {
const userCategoriesFilePath = cacheFolderPath + "/userCategories.json"
const userYieldUnitsFilePath = cacheFolderPath + "/userYieldUnits.json"
if (Folder.exists(cacheFolderPath)) {
this.isImportedDataValid([
this.isFileDataValid([
{
zipPath,
path: EnRecipesFilePath,
@ -341,30 +329,33 @@ export default {
} else {
Folder.fromPath(extractedFolderPath).remove()
Toast.makeText(
"Zip modified externally or incorrect file",
"Import failed. Backup file is incorrect or corrupt",
"long"
).show()
}
if (Folder.exists(cacheFolderPath + "/Images")) {
this.copyImages(cacheFolderPath + "/Images")
this.importImages(cacheFolderPath + "/Images")
}
})
},
copyImages(sourcePath) {
importImages(sourcePath) {
let dest = knownFolders.documents().path
Zip.unzip({
archive: sourcePath,
directory: dest,
overwrite: true,
}).then((res) => {
Toast.makeText("Import successful!", "long").show()
Toast.makeText("Import successful", "long").show()
this.$navigateBack()
})
},
permissionCheck(confirmation, action) {
// PERMISSIONS HANDLER
permissionCheck(confirmation, description, action) {
if (!ApplicationSettings.getBoolean("storagePermissionAsked", false)) {
confirmation().then((e) => {
confirmation(description).then((e) => {
if (e) {
Permissions.request("storage").then((res) => {
Permissions.request("photo").then((res) => {
let status = res[Object.keys(res)[0]]
if (status === "authorized") action()
if (status !== "denied")
@ -374,16 +365,26 @@ export default {
}
})
} else {
Permissions.request("storage").then((res) => {
Permissions.check("photo").then((res) => {
let status = res[Object.keys(res)[0]]
if (status !== "authorized") {
confirmation().then((e) => {
confirmation(description).then((e) => {
e && this.openAppSettingsPage()
})
} else action()
})
}
},
permissionConfirmation(description) {
return this.$showModal(ConfirmDialog, {
props: {
title: "Grant permission",
description,
cancelButtonText: "NOT NOW",
okButtonText: "CONTINUE",
},
})
},
},
created() {
this.appTheme = ApplicationSettings.getString("appTheme", "Light")

View file

@ -1,8 +1,9 @@
<template>
<Page @loaded="initializePage" @unloaded="unLoad">
<Page @loaded="onPageLoad" @unloaded="onPageUnload">
<ActionBar height="112" margin="0" flat="true">
<GridLayout rows="64, 48" columns="auto, *, auto,auto, auto">
<Label
<MDButton
variant="text"
row="0"
col="0"
class="bx"
@ -23,30 +24,36 @@
verticalAlignment="bottom"
/>
</ScrollView>
<Label
row="0"
col="3"
class="bx"
:text="recipe.isFavorite ? icon.heart : icon.heartOutline"
@tap="toggleFavorite"
/>
<Label
v-if="!filterTrylater"
row="0"
col="4"
class="bx"
:text="recipe.tried ? icon.trylaterOutline : icon.trylater"
@tap="toggleTrylater"
/>
<Label
v-if="!busy"
row="0"
col="2"
class="bx"
:text="icon.edit"
@tap="editRecipe"
/>
<ActivityIndicator v-else row="0" col="2" :busy="busy" />
<FlexboxLayout row="0" col="2" alignItems="center">
<MDButton
variant="text"
v-if="!busy"
class="bx"
:text="icon.edit"
@tap="editRecipe"
/>
<MDActivityIndicator v-else :busy="busy" />
<MDButton
variant="text"
class="bx"
:text="recipe.isFavorite ? icon.heart : icon.heartOutline"
@tap="toggleFavorite"
/>
<MDButton
variant="text"
v-if="!filterTrylater"
class="bx"
:text="recipe.tried ? icon.trylaterOutline : icon.trylater"
@tap="toggleTrylater"
/>
<MDButton
variant="text"
v-else
class="bx"
:text="icon.share"
@tap="shareHandler"
/>
</FlexboxLayout>
</GridLayout>
</ActionBar>
<AbsoluteLayout>
@ -91,27 +98,26 @@
:text="recipe.title"
textWrap="true"
/>
<Label class="time">
<FormattedString>
<Span text="Time required:"></Span>
<Span
:text="` ${formattedTime(recipe.timeRequired)}`"
></Span>
</FormattedString>
</Label>
<StackLayout orientation="horizontal" class="time">
<Label text="Time required:" />
<Label :text="` ${formattedTime(recipe.timeRequired)}`" />
</StackLayout>
<GridLayout
rows="auto, auto"
columns="*, *"
class="overviewContainer"
>
<StackLayout
<GridLayout
class="overviewItem"
row="0"
col="0"
@tap="selectedTabIndex = 1"
rows="auto, auto"
columns="*"
>
<Label class="bx" :text="icon.item" />
<MDRipple rowSpan="2" @tap="selectedTabIndex = 1" />
<Label row="0" class="bx" :text="icon.item" />
<Label
row="1"
class="itemCount"
:text="
`${recipe.ingredients.length} ${
@ -122,15 +128,18 @@
"
textWrap="true"
/>
</StackLayout>
<StackLayout
</GridLayout>
<GridLayout
class="overviewItem"
row="0"
col="1"
@tap="selectedTabIndex = 2"
rows="auto, auto"
columns="*"
>
<Label class="bx" :text="icon.step" />
<MDRipple rowSpan="2" @tap="selectedTabIndex = 2" />
<Label row="0" class="bx" :text="icon.step" />
<Label
row="1"
class="itemCount"
:text="
`${recipe.instructions.length} ${
@ -139,15 +148,18 @@
"
textWrap="true"
/>
</StackLayout>
<StackLayout
</GridLayout>
<GridLayout
class="overviewItem"
row="1"
col="0"
@tap="selectedTabIndex = 3"
rows="auto, auto"
columns="*"
>
<Label class="bx" :text="icon.note" />
<MDRipple rowSpan="2" @tap="selectedTabIndex = 3" />
<Label row="0" class="bx" :text="icon.note" />
<Label
row="1"
class="itemCount"
:text="
`${recipe.notes.length} ${
@ -156,15 +168,18 @@
"
textWrap="true"
/>
</StackLayout>
<StackLayout
</GridLayout>
<GridLayout
class="overviewItem"
row="1"
col="1"
@tap="selectedTabIndex = 4"
rows="auto, auto"
columns="*"
>
<Label class="bx" :text="icon.source" />
<MDRipple rowSpan="2" @tap="selectedTabIndex = 4" />
<Label row="0" class="bx" :text="icon.source" />
<Label
row="1"
class="itemCount"
:text="
`${recipe.references.length} ${
@ -175,7 +190,7 @@
"
textWrap="true"
/>
</StackLayout>
</GridLayout>
</GridLayout>
</StackLayout>
</StackLayout>
@ -185,11 +200,11 @@
<ScrollView scrollBarIndicatorVisible="false">
<GridLayout
v-if="!recipe.ingredients.length"
rows="96, auto, *"
rows="*, auto, *, 88"
columns="*"
class="emptyState"
class="emptyStateContainer"
>
<StackLayout col="0" row="1" class="noResult">
<StackLayout col="0" row="1" class="emptyState">
<Label class="bx icon" :text="icon.item" textWrap="true" />
<StackLayout orientation="horizontal" class="title orkm">
<Label text="Use the " />
@ -199,7 +214,7 @@
<Label text="to add some ingredients" textWrap="true" />
</StackLayout>
</GridLayout>
<StackLayout v-else padding="16 16 134">
<StackLayout v-else padding="32 16 134">
<AbsoluteLayout class="inputField">
<TextField
width="50%"
@ -212,7 +227,7 @@
:text="`Required ${recipe.yield.unit.toLowerCase()}`"
/>
</AbsoluteLayout>
<StackLayout margin="24 0 16 0">
<StackLayout margin="16 0">
<Label
class="title orkm"
:text="
@ -231,7 +246,7 @@
v-if="filterTrylater"
class="ingredient-check"
checkPadding="16"
:fillColor="`${isLightMode ? '#ff5200' : '#ff7043'}`"
fillColor="#ff5200"
:text="
`${
roundedQuantity(item.quantity)
@ -264,11 +279,11 @@
<ScrollView scrollBarIndicatorVisible="false">
<GridLayout
v-if="!recipe.instructions.length"
rows="96, auto, *"
rows="*, auto, *, 88"
columns="*"
class="emptyState"
class="emptyStateContainer"
>
<StackLayout col="0" row="1" class="noResult">
<StackLayout col="0" row="1" class="emptyState">
<Label class="bx icon" :text="icon.step" textWrap="true" />
<StackLayout orientation="horizontal" class="title orkm">
<Label text="Use the " />
@ -309,11 +324,11 @@
<ScrollView scrollBarIndicatorVisible="false">
<GridLayout
v-if="!recipe.notes.length"
rows="96, auto, *"
rows="*, auto, *, 88"
columns="*"
class="emptyState"
class="emptyStateContainer"
>
<StackLayout col="0" row="1" class="noResult">
<StackLayout col="0" row="1" class="emptyState">
<Label class="bx icon" :text="icon.note" textWrap="true" />
<StackLayout orientation="horizontal" class="title orkm">
<Label text="Use the " />
@ -351,11 +366,11 @@
<ScrollView scrollBarIndicatorVisible="false">
<GridLayout
v-if="!recipe.references.length"
rows="96, auto, *"
rows="*, auto, *, 88"
columns="*"
class="emptyState"
class="emptyStateContainer"
>
<StackLayout col="0" row="1" class="noResult">
<StackLayout col="0" row="1" class="emptyState">
<Label class="bx icon" :text="icon.source" textWrap="true" />
<StackLayout orientation="horizontal" class="title orkm">
<Label text="Use the " />
@ -375,8 +390,8 @@
columns="*, auto"
class="referenceItem"
androidElevation="1"
@longPress="copyURL($event, reference)"
>
<MDRipple colSpan="3" @tap="copyURL(reference)" />
<Label
col="0"
verticalAlignment="center"
@ -384,11 +399,13 @@
:text="reference"
textWrap="false"
/>
<Label
<MDButton
variant="text"
automationText="openURL"
col="1"
class="bx"
:text="icon.source"
@tap="openURL($event, reference)"
@tap="openURL(reference)"
/>
</GridLayout>
<Label
@ -402,22 +419,20 @@
</ScrollView>
</TabViewItem>
</TabView>
<GridLayout id="btnFabContainer" rows="*,auto" columns="*,auto">
<Label
<GridLayout id="btnFabContainer" rows="*, auto" columns="*, auto">
<MDFloatingActionButton
row="1"
col="1"
class="bx fab-button"
:text="icon.check"
src="res://check"
@tap="recipeTried"
v-if="filterTrylater"
/>
<transition name="dolly" appear>
<Label
<MDFloatingActionButton
row="1"
col="1"
class="bx fab-button"
:text="icon.share"
@tap="shareRecipe"
src="res://share"
@tap="shareHandler"
v-if="!filterTrylater && showFab"
/>
</transition>
@ -428,22 +443,24 @@
<script>
import {
Screen,
Utils,
ImageSource,
Color,
Device,
File,
Color,
knownFolders,
path,
ImageSource,
Screen,
Utils,
} from "@nativescript/core"
import { Feedback, FeedbackType, FeedbackPosition } from "nativescript-feedback"
import * as Toast from "nativescript-toast"
import * as SocialShare from "nativescript-social-share-ns-7"
import * as SocialShare from "@nativescript/social-share"
import { setText } from "nativescript-clipboard"
import { Application } from "@nativescript/core"
import { mapState, mapActions } from "vuex"
import { mapActions, mapState } from "vuex"
import EditRecipe from "./EditRecipe.vue"
import ShareChooser from "./modal/ShareChooser.vue"
let feedback = new Feedback()
@ -480,7 +497,7 @@ export default {
},
methods: {
...mapActions(["toggleStateAction", "setCurrentComponentAction"]),
initializePage() {
onPageLoad() {
this.releaseGlobalBackEvent()
this.busy = false
setTimeout((e) => {
@ -488,8 +505,14 @@ export default {
}, 500)
this.yieldMultiplier = this.recipe.yield.quantity
this.showFab = true
this.toggleScreenOn(true)
this.keepScreenOn(true)
},
onPageUnload() {
feedback.hide()
this.keepScreenOn(false)
},
// HELPERS
niceDates(time) {
let lastTried = new Date(time).getTime()
let now = new Date().getTime()
@ -509,26 +532,15 @@ export default {
selectedIndexChange(args) {
this.selectedTabIndex = args.object.selectedIndex
},
showInfo() {
showLastTried() {
feedback.show({
title: `You tried this recipe ${this.niceDates(
this.recipe.lastTried
)}!`,
titleColor: new Color(`${this.isLightMode ? "#fff" : "#111"}`),
backgroundColor: new Color(
`${this.isLightMode ? "#ff5200" : "#ff7043"}`
),
titleColor: new Color(`${this.isLightMode ? "#f1f3f5" : "#212529"}`),
backgroundColor: new Color("#ff5200"),
})
},
unLoad() {
feedback.hide()
this.toggleScreenOn(false)
},
highlight(args) {
let temp = args.object.className
args.object.className = `${temp} option-highlight`
setTimeout(() => (args.object.className = temp), 100)
},
roundedQuantity(quantity) {
return (
Math.round(
@ -538,6 +550,32 @@ export default {
) / 100
)
},
keepScreenOn(boolean) {
let activity =
Application.android.foregroundActivity ||
Application.android.startActivity
let window = activity.getWindow()
if (boolean)
window.addFlags(
android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
)
else
window.clearFlags(
android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
)
},
formattedTime(time) {
let t = time.split(":")
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")
return pattern.test(string)
},
// NAVIGATION HANDLERS
editRecipe() {
this.showFab = false
this.busy = true
@ -554,6 +592,47 @@ export default {
// backstackVisible: false,
})
},
// SHARE ACTION
shareHandler() {
if (this.recipe.imageSrc) {
this.$showModal(ShareChooser, {
props: {
title: "Share",
},
}).then((result) => {
switch (result) {
case "photo":
// let cacheFilePath = path.join(
// knownFolders.temp().path,
// `${this.recipe.title}.jpg`
// )
// if (!File.exists(cacheFilePath)) {
// File.fromPath(cacheFilePath).writeSync(
// File.fromPath(this.recipe.imageSrc).readSync()
// )
// }
// let shareFile = new ShareFile()
// shareFile.open({
// path: cacheFilePath,
// title: "Share recipe photo using",
// })
ImageSource.fromFile(this.recipe.imageSrc).then((res) => {
SocialShare.shareImage(res, "Share recipe photo using")
})
break
case "recipe":
this.shareRecipe()
break
default:
break
}
})
} else {
this.shareRecipe()
}
},
shareRecipe() {
let overview = `${
this.recipe.title
@ -564,9 +643,11 @@ export default {
this.yieldMultiplier
} ${this.recipe.yield.unit.toLowerCase()}\n\n`
this.recipe.ingredients.forEach((e) => {
ingredients += `- ${this.roundedQuantity(e.quantity)} ${e.unit} ${
e.item
}\n`
ingredients += `- ${
e.quantity
? this.roundedQuantity(e.quantity) + " " + e.unit + " "
: ""
}${e.item}\n`
})
shareContent += ingredients
}
@ -592,15 +673,14 @@ export default {
shareContent += references
}
let sharenote =
"\nCreated and shared via EnRecipes.\nDownload the app on f-droid: https://www.vishnuraghav.com/"
"\nCreated and shared via EnRecipes.\nGet it on F-Droid."
shareContent += sharenote
SocialShare.shareText(
shareContent,
"How would you like to share this recipe?"
)
SocialShare.shareText(shareContent, "Share recipe using")
},
// DATA HANDLERS
toggle(key, setDate) {
this.toggleStateAction({
index: this.recipeIndex,
@ -624,53 +704,32 @@ export default {
: this.filterTrylater
? Toast.makeText("You tried this recipe").show()
: Toast.makeText("Removed from Try later").show()
// : Toast.makeText("You tried this recipe").show()
this.toggle("tried")
},
recipeTried() {
this.toggle("tried", true)
this.$navigateBack()
},
formattedTime(time) {
let t = time.split(":")
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")
return pattern.test(string)
},
openURL(args, url) {
// this.highlight(args)
// URL ACTION
openURL(url) {
Utils.openUrl(url)
},
copyURL(args, url) {
copyURL(url) {
setText(url).then((e) => {
Toast.makeText("URL Copied").show()
})
},
toggleScreenOn(boolean) {
let activity =
Application.android.foregroundActivity ||
Application.android.startActivity
let window = activity.getWindow()
if (boolean)
window.addFlags(
android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
)
else
window.clearFlags(
android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
)
},
},
created() {
this.recipe = this.recipes.filter((e) => e.id === this.recipeID)[0]
},
mounted() {
this.showFab = true
setTimeout((e) => this.recipe.tried && this.showInfo(), 500)
setTimeout(
(e) => this.recipe.tried && this.recipe.lastTried && this.showLastTried(),
500
)
},
}
</script>

View file

@ -2,26 +2,33 @@
<Page>
<StackLayout class="dialogContainer" :class="appTheme">
<Label class="dialogTitle orkm" :text="title" />
<ListView
width="100%"
:height="height"
for="item in list"
@itemTap="tapAction"
separatorColor="transparent"
>
<v-template>
<Label class="actionItem" :text="item" />
</v-template>
</ListView>
<ScrollView width="100%" :height="height ? height : screenHeight - 320">
<StackLayout>
<MDButton
v-for="(item, index) in list"
:key="index"
class="actionItem"
variant="text"
:rippleColor="rippleColor"
:text="item"
@loaded="onLabelLoaded"
@tap="tapAction(item)"
/>
</StackLayout>
</ScrollView>
<GridLayout rows="auto" columns="auto, *, auto" class="actionsContainer">
<Label
<MDButton
:rippleColor="rippleColor"
variant="text"
v-if="action"
col="0"
class="action orkm pull-left"
:text="action"
@tap="$modal.close(action)"
/>
<Label
<MDButton
:rippleColor="rippleColor"
variant="text"
col="2"
class="action orkm pull-right"
text="CANCEL"
@ -33,18 +40,29 @@
</template>
<script>
import { Application } from "@nativescript/core"
import { Application, Screen } from "@nativescript/core"
export default {
props: ["title", "list", "height", "action"],
computed: {
appTheme() {
return Application.systemAppearance()
},
rippleColor() {
return this.appTheme == "light"
? "rgba(134,142,150,0.2)"
: "rgba(206,212,218,0.1)"
},
screenHeight() {
return Math.round(Screen.mainScreen.heightDIPs)
},
},
methods: {
tapAction({ item }) {
tapAction(item) {
this.$modal.close(item)
},
onLabelLoaded(args) {
args.object.android.setGravity(16)
},
},
}
</script>

View file

@ -1,6 +1,6 @@
<template>
<Page>
<StackLayout class="dialogContainer" :class="isLightMode">
<StackLayout class="dialogContainer" :class="appTheme">
<Label class="dialogTitle orkm" :text="title" />
<Label
v-if="description"
@ -8,19 +8,19 @@
:text="description"
textWrap="true"
/>
<GridLayout
rows="auto"
columns="*, auto, 32, auto"
class="actionsContainer"
>
<Label
<GridLayout rows="auto" columns="*, auto, auto" class="actionsContainer">
<MDButton
:rippleColor="rippleColor"
variant="text"
col="1"
class="action orkm"
:text="cancelButtonText"
@tap="$modal.close(false)"
/>
<Label
col="3"
<MDButton
:rippleColor="rippleColor"
variant="text"
col="2"
class="action orkm"
:text="okButtonText"
@tap="$modal.close(true)"
@ -35,9 +35,14 @@ import { Application } from "@nativescript/core"
export default {
props: ["title", "description", "cancelButtonText", "okButtonText"],
computed: {
isLightMode() {
appTheme() {
return Application.systemAppearance()
},
rippleColor() {
return this.appTheme == "light"
? "rgba(134,142,150,0.2)"
: "rgba(206,212,218,0.1)"
},
},
}
</script>

View file

@ -1,6 +1,6 @@
<template>
<Page>
<StackLayout class="dialogContainer" :class="isLightMode">
<StackLayout class="dialogContainer" :class="appTheme">
<Label class="dialogTitle orkm" :text="title" />
<StackLayout
class="dialogListPicker"
@ -13,12 +13,6 @@
:selectedIndex="hrIndex"
@selectedIndexChange="setHrs"
></ListPicker>
<Label
verticalAlignment="center"
class="okrb"
text=":"
textWrap="false"
/>
<ListPicker
ref="minPicker"
:items="mins"
@ -26,19 +20,19 @@
@selectedIndexChange="setMins"
></ListPicker>
</StackLayout>
<GridLayout
rows="auto"
columns="*, auto, 32, auto"
class="actionsContainer"
>
<Label
<GridLayout rows="auto" columns="*, auto, auto" class="actionsContainer">
<MDButton
:rippleColor="rippleColor"
variant="text"
col="1"
class="action orkm"
text="CANCEL"
@tap="$modal.close(false)"
/>
<Label
col="3"
<MDButton
:rippleColor="rippleColor"
variant="text"
col="2"
class="action orkm"
:text="action"
@tap="$modal.close(selectedTime)"
@ -55,52 +49,52 @@ export default {
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",
"0h",
"1h",
"2h",
"3h",
"4h",
"5h",
"6h",
"7h",
"8h",
"9h",
"10h",
"11h",
"12h",
"13h",
"14h",
"15h",
"16h",
"17h",
"18h",
"19h",
"20h",
"21h",
"22h",
"23h",
],
mins: [
"00",
"01",
"02",
"03",
"04",
"05",
"06",
"07",
"08",
"09",
"10",
"15",
"20",
"25",
"30",
"35",
"40",
"45",
"50",
"55",
"0m",
"1m",
"2m",
"3m",
"4m",
"5m",
"6m",
"7m",
"8m",
"9m",
"10m",
"15m",
"20m",
"25m",
"30m",
"35m",
"40m",
"45m",
"50m",
"55m",
],
selectedHrs: "00",
selectedMins: "00",
@ -108,24 +102,37 @@ export default {
},
computed: {
hrIndex() {
return this.hrs.indexOf(this.selectedHr)
let hr = this.selectedHr
if (hr.charAt(0) == "0") hr = hr.slice(-1) + "h"
else hr = hr + "h"
return this.hrs.indexOf(hr)
},
minIndex() {
return this.mins.indexOf(this.selectedMin)
let min = this.selectedMin
if (min.charAt(0) == "0") min = min.slice(-1) + "m"
else min = min + "m"
return this.mins.indexOf(min)
},
isLightMode() {
appTheme() {
return Application.systemAppearance()
},
rippleColor() {
return this.appTheme == "light"
? "rgba(134,142,150,0.2)"
: "rgba(206,212,218,0.1)"
},
selectedTime() {
return this.selectedHrs + ":" + this.selectedMins
},
},
methods: {
setHrs(args) {
this.selectedHrs = this.hrs[args.object.selectedIndex]
let hr = "0" + this.hrs[args.object.selectedIndex]
this.selectedHrs = hr.slice(-3).slice(0, -1)
},
setMins(args) {
this.selectedMins = this.mins[args.object.selectedIndex]
let min = "0" + this.mins[args.object.selectedIndex]
this.selectedMins = min.slice(-3).slice(0, -1)
},
},
}

View file

@ -1,28 +1,28 @@
<template>
<Page>
<StackLayout class="dialogContainer" :class="isLightMode">
<StackLayout class="dialogContainer" :class="appTheme">
<Label class="dialogTitle orkm" :text="title" />
<StackLayout class="dialogInput">
<TextField
@loaded="focusField"
:hint="hint"
:hint="hint ? hint : ''"
v-model="category"
autocapitalizationType="words"
/>
</StackLayout>
<GridLayout
rows="auto"
columns="*, auto, 32, auto"
class="actionsContainer"
>
<Label
<GridLayout rows="auto" columns="*, auto, auto" class="actionsContainer">
<MDButton
:rippleColor="rippleColor"
variant="text"
col="1"
class="action orkm"
text="CANCEL"
@tap="$modal.close(false)"
/>
<Label
col="3"
<MDButton
:rippleColor="rippleColor"
variant="text"
col="2"
class="action orkm"
:text="action"
@tap="$modal.close(category)"
@ -35,16 +35,21 @@
<script>
import { Application, Utils } from "@nativescript/core"
export default {
props: ["title", "hint", "action"],
props: ["title", "hint", "text", "action"],
data() {
return {
category: null,
}
},
computed: {
isLightMode() {
appTheme() {
return Application.systemAppearance()
},
rippleColor() {
return this.appTheme == "light"
? "rgba(134,142,150,0.2)"
: "rgba(206,212,218,0.1)"
},
},
methods: {
focusField(args) {
@ -52,5 +57,10 @@ export default {
setTimeout((e) => Utils.ad.showSoftInput(args.object.android), 1)
},
},
mounted() {
if (this.text) {
this.category = this.text
}
},
}
</script>

View file

@ -0,0 +1,49 @@
<template>
<Page>
<StackLayout class="dialogContainer" :class="appTheme">
<Label class="dialogTitle orkm" :text="title" />
<GridLayout rows="auto, auto" columns="*" class="actionsContainer">
<MDButton
:rippleColor="rippleColor"
:backgroundColor="backgroundColor"
row="0"
class="actionIcon"
src="res://photo"
text="Photo"
@tap="$modal.close('photo')"
/>
<MDButton
:rippleColor="rippleColor"
:backgroundColor="backgroundColor"
row="1"
class="actionIcon"
src="res://detail"
text="Recipe"
@tap="$modal.close('recipe')"
/>
</GridLayout>
</StackLayout>
</Page>
</template>
<script>
import { Application } from "@nativescript/core"
import { mapState } from "vuex"
export default {
props: ["title"],
computed: {
...mapState(["icon"]),
appTheme() {
return Application.systemAppearance()
},
rippleColor() {
return this.appTheme == "light"
? "rgba(134,142,150,0.2)"
: "rgba(206,212,218,0.1)"
},
backgroundColor() {
return this.appTheme == "light" ? "#fff" : "#343a40"
},
},
}
</script>

View file

@ -5,6 +5,24 @@ import store from "./store"
import RadListView from "nativescript-ui-listview/vue"
Vue.use(RadListView)
import ButtonPlugin from "@nativescript-community/ui-material-button/vue"
Vue.use(ButtonPlugin)
import ActivityIndicatorPlugin from "@nativescript-community/ui-material-activityindicator/vue"
Vue.use(ActivityIndicatorPlugin)
import RipplePlugin from "@nativescript-community/ui-material-ripple/vue"
Vue.use(RipplePlugin)
import FloatingActionButtonPlugin from "@nativescript-community/ui-material-floatingactionbutton/vue"
Vue.use(FloatingActionButtonPlugin)
import ProgressPlugin from "@nativescript-community/ui-material-progress/vue"
Vue.use(ProgressPlugin)
// import SpeedDialPlugin from "@nativescript-community/ui-material-speeddial/vue"
// Vue.use(SpeedDialPlugin)
Vue.registerElement(
"RadSideDrawer",
() => require("nativescript-ui-sidedrawer").RadSideDrawer

View file

@ -147,6 +147,7 @@ export default new Vuex.Store({
heart: "\ued36",
heartOutline: "\uea6c",
label: "\ued51",
labelOutline: "\uea8b",
cog: "\ueca6",
info: "\ued49",
menu: "\ueac1",
@ -165,6 +166,7 @@ export default new Vuex.Store({
theme: "\uecaa",
link: "\ueaa0",
file: "\ued02",
detail: "\ue9f9",
user: "\uee33",
trash: "\uee26",
donate: "\ueb4f",
@ -185,9 +187,8 @@ export default new Vuex.Store({
},
mutations: {
initializeRecipes(state) {
let a = EnRecipesDB.query({ select: [] })
a.forEach((e) => {
state.recipes.push(e)
EnRecipesDB.query({ select: [] }).forEach((recipe) => {
state.recipes.push(recipe)
})
},
initializeCategories(state) {

View file

@ -1,17 +1,51 @@
require("tns-core-modules/globals")
import { ImageSource, ImageAsset } from "@nativescript/core"
import { ImageSource } from "@nativescript/core"
var ImageCropper = require("nativescript-imagecropper").ImageCropper
global.onmessage = function({ data }) {
console.log(data)
let imgSavedToPath = data.imgSavedToPath
let imgAsset = new ImageAsset(data.imgFile)
imgAsset.options = {
width: 1200,
height: 1200,
keepAspectRatio: true,
}
ImageSource.fromAsset(imgAsset).then((imgData) => {
if (imgData.saveToFile(imgSavedToPath, "jpg", 75)) {
global.postMessage("savedToFile")
}
let imgPath = data.imgPath
let screenWidth = data.screenWidth
let toolbarTextColor = data.toolbarTextColor
let toolbarColor = data.toolbarColor
ImageSource.fromFile(imgPath).then((image) => {
ImageCropper.prototype
.show(
image,
{
width: screenWidth,
height: screenWidth,
// compressionQuality: 75,
},
{
hideBottomControls: true,
toolbarTitle: "Crop photo",
statusBarColor: "#ff5200",
toolbarTextColor,
toolbarColor,
cropFrameColor: "#ff5200",
}
)
.then((cropped) => {
if (cropped.image.saveToFile(imgSavedToPath, "jpg", 75))
global.postMessage("savedToFile")
})
})
}
// global.onmessage = function({ data }) {
// let imgSavedToPath = data.imgSavedToPath
// let imgAsset = new ImageAsset(data.imgFile)
// imgAsset.options = {
// width: 1200,
// height: 1200,
// keepAspectRatio: true,
// }
// ImageSource.fromAsset(imgAsset).then((imgData) => {
// if (imgData.saveToFile(imgSavedToPath, "jpg", 75)) {
// global.postMessage("savedToFile")
// }
// })
// }

2490
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,17 +9,22 @@
},
"dependencies": {
"@nativescript-community/perms": "^2.1.1",
"@nativescript-community/ui-material-activityindicator": "^5.0.30",
"@nativescript-community/ui-material-button": "^5.1.0",
"@nativescript-community/ui-material-floatingactionbutton": "^5.0.30",
"@nativescript-community/ui-material-progress": "^5.0.30",
"@nativescript-community/ui-material-ripple": "^5.0.30",
"@nativescript/core": "~7.0.0",
"@nativescript/imagepicker": "^1.0.0",
"@nativescript/social-share": "^2.0.1",
"@nativescript/theme": "^3.0.0",
"@nativescript/webpack": "3.0.0",
"@nativescript/zip": "^5.0.0",
"@nstudio/nativescript-checkbox": "^2.0.4",
"nativescript-clipboard": "^2.0.0",
"nativescript-couchbase-plugin": "^0.9.6",
"nativescript-feedback": "^2.0.0",
"nativescript-mediafilepicker": "^4.0.1",
"nativescript-imagecropper": "^4.0.1",
"nativescript-plugin-filepicker": "^1.0.0",
"nativescript-social-share-ns-7": "^11.6.0",
"nativescript-toast": "^2.0.0",
"nativescript-ui-listview": "^9.0.4",
"nativescript-ui-sidedrawer": "^9.0.3",
@ -34,7 +39,8 @@
"babel-loader": "^8.1.0",
"nativescript-vue-template-compiler": "^2.6.0",
"node-sass": "^4.13.1",
"vue-loader": "^15.9.1"
"vue-loader": "^15.9.1",
"@nativescript/webpack": "~3.0.0"
},
"main": "main"
}