diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2c6f380..825dd66e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,4 +6,4 @@ Here are some important resources: - The [roadmap](https://github.com/vishnuraghavb/EnRecipes/projects/1) will tell you whats the future of EnRecipes. Your feedback and suggestions are very important to make EnRecipes the best. If you have an idea to improve EnRecipes, [do let me know](https://github.com/vishnuraghavb/EnRecipes#having-issues-suggestions-and-feedback). I'm always open to ideas ;) - You can help [translate EnRecipes using Weblate](https://hosted.weblate.org/engage/enrecipes/) into the language of your choice. See [translation instructions](https://github.com/vishnuraghavb/EnRecipes/wiki/Translation-Instructions) in the wiki for more information. -- Bugs, suggestions or feedback? You can [create an issue here](https://github.com/vishnuraghavb/EnRecipes/issues) or [join the Telegram group](http://t.me/enrecipes)(quicker replies) or contact me at apps@vishnuraghav.com +- Bugs, suggestions or feedback? You can [create an issue here](https://github.com/vishnuraghavb/EnRecipes/issues) or [join the Telegram group](http://t.me/enrecipes) (quicker replies) or contact me at apps@vishnuraghav.com diff --git a/README.md b/README.md index 5a0978cc..b29fcc19 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Translation status

-
You can also get the latest release on GitHub +
You can also get the latest release on GitHub

Enjoying EnRecipes?

Please consider making a small donation to help fund the project. Developing an application, especially one that is open source and completely free, takes a lot of time and effort. @@ -28,16 +28,17 @@ - Scale your recipe ingredients to serve more or less people - Get notified of the last time you tried a recipe - Share your recipe to anyone by any means as a nicely formatted message. You can share the recipe photo too. -- Shake device to view random recipe +- Shake your device to view a random recipe - Create meal plans -- Import/Export recipes -- Light, Dark and Black themes +- Set cooking timers +- You can Import or Export your data +- Has Light, Dark and Black themes ## Highlights - 100% free and open-source - Private by Design -- No permissions required +- No special permissions required - No annoying ads or pop-ups **Languages being translated**: diff --git a/app/app.scss b/app/app.scss index ae66c777..ddb33624 100644 --- a/app/app.scss +++ b/app/app.scss @@ -1,3 +1,4 @@ +// Colours $gray0: #f8f9fa; $gray1: #f1f3f5; $gray2: #e9ecef; @@ -11,21 +12,29 @@ $gray9: #212529; $gray10: #000000; $orange: #ff5200; +// FontSizes based on Minor Third +$t1: 25; +$t2: 21; +$t3: 17; +$t4: 14; +$t5: 12; // Base size +$t6: 10; + Page { - font-family: 'Inter-Medium'; - font-size: 14; + font-family: 'Inter-Medium', sans-serif; + font-size: $t4; } .ico { font-family: 'enrecipes'; font-size: 24; vertical-alignment: center; &.sm { - font-size: 16; + font-size: $t3; opacity: 0.5; } } .tb { - font-family: 'Inter-Bold'; + font-family: 'Inter-Bold', sans-serif; } .tac { text-align: center; @@ -36,7 +45,7 @@ Page { .pageTitle { @extend .tb; @extend .tw; - font-size: 25; + font-size: $t1; padding: 16 16 24; } .Light { @@ -60,7 +69,6 @@ Page { background: $gray0; } .fieldLabel, - .dayName, .sub { color: $gray6; } @@ -98,7 +106,6 @@ Page { background: $gray8; } .fieldLabel, - .dayName, .sub { color: $gray5; } @@ -136,7 +143,6 @@ Page { background: $gray9; } .fieldLabel, - .dayName, .sub { color: $gray6; } @@ -169,14 +175,13 @@ TextView { line-height: 4; } #searchBar { - padding-left: 0; - margin: 0; + padding: 13 12; } .inputField { margin-bottom: 24; } .fieldLabel { - font-size: 12; + font-size: $t5; } .progressContainer { width: 100%; @@ -198,25 +203,28 @@ button { border-radius: 12; min-width: 0; min-height: 0; - &:active { - @extend .fade; - } &.ico { width: 48; height: 48; padding: 0; margin: 0; + &:active { + @extend .fade; + } } &.text { @extend .tb; color: $orange; + &:active { + @extend .fade; + } } &.big { margin-top: 8; padding: 16 0; } &.sm { - font-size: 12; + font-size: $t5; padding: 12; } &.min { @@ -224,10 +232,16 @@ button { height: 40; vertical-alignment: center; } + &.fb:active { + @extend .fade; + } &.rate { margin: 0 4 0 0; width: 32; height: 32; + &:active { + @extend .fade; + } } } ActivityIndicator { @@ -246,14 +260,15 @@ ActivityIndicator { .value { padding: 0 0 0 8; vertical-alignment: center; - &.rtl { + &.r { padding: 0 8 0 0; + transform: scaleX(-1); } } - &.select { - color: $orange; - @extend .hl; - } +} +.select { + color: $orange; + @extend .hl; } .emptyState { padding: 16 16 8; @@ -262,7 +277,7 @@ ActivityIndicator { } .title { @extend .tb; - font-size: 17; + font-size: $t3; } } @@ -272,22 +287,18 @@ ActivityIndicator { padding: 8 16; .recipeInfo { vertical-alignment: center; - padding: 0 0 4 8; - &.rtl { - transform: none; - padding: 0 8 4 0; - } + padding: 0 8 4; } .title { padding: 0 0 4; } - .attr { - font-size: 10; - padding: 0 6 1 2; - &.rtl { - padding: 0 2 1 6; - } - } +} +.attrs { + orientation: horizontal; +} +.attr { + font-size: $t6; + padding: 1 4; } .simple .recipeInfo { padding: 8 0; @@ -343,20 +354,24 @@ ActivityIndicator { .group-info { padding: 16 16 16 72; line-height: 4; + &.r { + padding: 16 72 16 16; + } } .options { .option { vertical-align: center; - padding: 14 12; + padding: 14 16; .ico { - margin: 0 24 0 12; + width: 40; + text-align: center; } - .info, - .sub { + .info { + padding: 0 16; @extend .tw; } .sub { - font-size: 12; + font-size: $t5; } } } @@ -371,7 +386,7 @@ ActivityIndicator { horizontal-alignment: center; } .name { - font-size: 21; + font-size: $t2; } .info { padding: 8 16 24; @@ -394,9 +409,8 @@ ActivityIndicator { } .attribute { margin: 8 16; - .title { - margin-right: 8; - font-size: 12; + .sub { + font-size: $t5; } .value { @extend .tb; @@ -416,7 +430,7 @@ ActivityIndicator { padding: 0 16; .count { @extend .tb; - font-size: 17; + font-size: $t3; } .value { @extend .tw; @@ -443,7 +457,7 @@ ActivityIndicator { } .dateInfo { padding: 32 16 16; - font-size: 12; + font-size: $t5; line-height: 4; } @@ -459,10 +473,12 @@ ActivityIndicator { @extend .tb; @extend .tw; vertical-align: center; + margin: 0 12; line-height: 4; } .msg { padding: 14 16; + margin: 0; } .fab { margin-left: 8; @@ -471,21 +487,21 @@ ActivityIndicator { margin: 8 8 0; } } +.sidebar { + margin-bottom: 0; +} .toolbar { z-index: 4; padding: 4; margin: 0 0 52; - horizontal-alignment: left; .tool { - padding: 0 12; + padding: 0 8; label { vertical-alignment: center; } + .value, .ico { - padding: 0 8 0 0; - &.rtl { - padding: 0 0 0 8; - } + padding: 0 4; } } } @@ -499,7 +515,7 @@ ActivityIndicator { .sectionTitle { @extend .tb; @extend .tw; - font-size: 21; + font-size: $t2; padding: 0; margin: 32 0 16; } @@ -509,49 +525,51 @@ ActivityIndicator { margin: 0; } .countdown { - font-size: 17; + font-size: $t3; color: $orange; } // ----------------------------- // MealPlanner -.calendar { - padding: 0 8; - .navBtn { - margin: 0; - } - .monthName { - text-align: center; +.monthSwitcher { + padding: 0 16; + .month { vertical-alignment: center; - font-size: 17; - } - .dayName { - margin: 8 0; - font-size: 12; text-align: center; - } - .day { - border-radius: 12; - } - .hasPlans { - color: $orange; - } - .activeDay { - @extend .hl; + font-size: $t3; } } -.dayPlan { - padding: 16 16 80; - width: 100%; - .periodLabel { - font-size: 17; - text-transform: capitalize; - vertical-align: center; +.calendar { + padding: 0 16; + .dayName { + vertical-alignment: center; + text-align: center; + font-size: $t5; } - .recipeTitle { - @extend .tw; - padding: 16 8; - line-height: 4; + .accent.sub { + color: rgba($orange, 0.5); + } +} +.plans { + padding: 8 16 80; + width: 100%; + .date { + font-size: $t2; + padding: 16 0; + } + .plan { + padding: 8 0; + } + .meal { + font-size: $t3; + padding: 8 0; + } + .planContent { + min-height: 48; + padding: 8; + } + .attr { + padding: 0; } } @@ -566,7 +584,7 @@ ActivityIndicator { @extend .tb; @extend .tw; padding: 16; - font-size: 21; + font-size: $t2; } .input { padding: 0 16 8; @@ -581,13 +599,12 @@ ActivityIndicator { margin: 16 0; } .listItem { - @extend .tw; letter-spacing: 0; text-transform: none; line-height: 4; padding: 13 16; margin: 0; - background: transparent; + background-color: transparent; } .shareItem { border-radius: 12; @@ -649,10 +666,10 @@ ActivityIndicator { // ----------------------------- // Helpers -.rtl { +.f { transform: scaleX(-1); } -.clickable { +.accent { color: $orange; } .hal { diff --git a/app/components/CookingTimer.vue b/app/components/CookingTimer.vue index 1f70c6a7..d0e4351f 100644 --- a/app/components/CookingTimer.vue +++ b/app/components/CookingTimer.vue @@ -1,53 +1,55 @@ @@ -58,13 +60,17 @@ import { Observable, CoreTypes, Application, - ApplicationSettings, - AndroidApplication, Utils, Device, + Frame, } from "@nativescript/core"; +import { + getNumber, + setNumber, + remove, +} from "@nativescript/core/application-settings"; import { mapState, mapActions } from "vuex"; - +import EnRecipes from "./EnRecipes.vue"; import Action from "./modals/Action.vue"; import CTSettings from "./settings/CTSettings.vue"; import TimePickerHMS from "./modals/TimePickerHMS.vue"; @@ -72,13 +78,10 @@ import TimerReminder from "./modals/TimerReminder.vue"; import Timer from "./sub/Timer.vue"; import Toast from "./sub/Toast.vue"; import SnackBar from "./sub/SnackBar.vue"; - import * as utils from "~/shared/utils"; -import { EventBus } from "~/main"; -let undoTimer, - firingTimers = []; -declare const com: any; - +import { EvtBus } from "~/main"; +let barTimer; +declare const com, android: any; export default { components: { Timer, Toast, SnackBar }, props: ["recipeID"], @@ -86,43 +89,62 @@ export default { return { scrollPos: 1, appbar: null, + toastbar: null, + snackbar: null, + scrollView: null, countdown: 5, snackMsg: null, - showUndo: false, - undo: false, - CTSettings: CTSettings, + showUndo: 0, + undo: 0, toast: null, + key: 99, }; }, computed: { ...mapState([ "icon", "recipes", - "currentComponent", "timerSound", "timerVibrate", - "timerDelay", "timerPresets", "activeTimers", + "FGService", + "RTL", ]), + hasBackStack() { + return Frame.topmost().backStack.length; + }, }, methods: { ...mapActions([ - "setComponent", "addActiveTimer", "removeActiveTimer", "clearTimerInterval", "addTimerPreset", "updateActiveTimer", + "setFGService", ]), - onPageLoad({ object }) { + pgLoad({ object }) { object.bindingContext = new Observable(); - this.setComponent("CookingTimer"); + if (this.activeTimers.filter((e: any) => e.done).length) + this.openReminder(); + this.keepScreenOnCountUp(); + setNumber("isTimer", 1); }, - onAppBarLoad({ object }) { + pgUnload() { + utils.keepScreenOn(0); + }, + abLoad({ object }) { this.appbar = object; }, - onScroll(args) { + tbLoad({ object }) { + this.toastbar = object; + }, + sbLoad({ object }) { + this.snackbar = object; + }, + svScroll(args) { + this.scrollView = args.object; let scrollUp; let y = args.scrollY; if (y) { @@ -132,18 +154,19 @@ export default { if (!scrollUp && ab == 0) { this.appbar.animate({ translate: { x: 0, y: 64 }, - duration: 250, + duration: 200, curve: CoreTypes.AnimationCurve.ease, }); } else if (scrollUp && ab == 64) { this.appbar.animate({ translate: { x: 0, y: 0 }, - duration: 250, + duration: 200, curve: CoreTypes.AnimationCurve.ease, }); } } }, + // HELPERS getRecipeTitle(id) { let recipe = this.recipes.filter((e) => e.id === id)[0]; @@ -168,81 +191,111 @@ export default { }, // NOTIFICATION HANDLERS - notifyTimers() { + timerInfo() { let activeCount = this.activeTimers.length; let pausedCount = this.activeTimers.filter((e) => e.isPaused).length; let ongoingCount = activeCount - pausedCount; - console.log("notifying"); - utils.TimerNotif.show({ - bID: "bringToFront", - cID: "cti", - cName: "Cooking Timer info", - description: `${ongoingCount} ongoing, ${pausedCount} paused`, - nID: 777, - priority: -2, - sound: null, - title: localize("timer"), - }); - if (activeCount <= 0) this.foregroundService(false); + this.foregroundService(activeCount); + function show() { + utils.TimerNotif.show({ + bID: "info", + cID: "cti", + cName: "Cooking Timer info", + description: localize("oAP", ongoingCount + "", pausedCount), + nID: 6, + priority: -2, + sound: null, + title: localize("timer"), + }); + } + if (this.FGService) + setTimeout(() => this.activeTimers.length && show(), 250); + this.keepScreenOnCountUp(); + utils.wakeLock(ongoingCount); }, - fireTimer(timer) { - console.log("firing"); - let description = timer.recipeID - ? " - " + this.getRecipeTitle(timer.recipeID) - : ""; - let title = timer.label; - let time = this.formattedTime(timer.time); - let bID = "timer" + timer.id; + timerAlert() { + let title, description, bID; + let firedTimers = this.activeTimers.filter((e) => e.done); + let timer = firedTimers[0]; + if (firedTimers.length > 1) { + title = localize("texp", firedTimers.length); + description = localize("ttv"); + bID = "alerts"; + } else if (firedTimers.length == 1) { + title = + timer.label + + (timer.recipeID ? " - " + this.getRecipeTitle(timer.recipeID) : ""); + description = this.formattedTime(timer.time); + bID = "timer" + timer.id; + } else { + utils.TimerNotif.clear(7); + return; + } utils.TimerNotif.show({ - actions: true, + actions: 1, bID, cID: "cta", cName: "Cooking Timer alerts", - description: time, - nID: timer.id, + description, + multi: firedTimers.length > 1, + nID: 7, priority: 1, sound: this.timerSound.uri, - title: title + description, + title, vibrate: this.timerVibrate, }); - Application.android.registerBroadcastReceiver(bID, (ctx, intent) => { - let action = intent.getStringExtra("action"); - console.log(action, "firing"); - EventBus.$emit(bID, action); + if (firedTimers.length == 1) { + Application.android.registerBroadcastReceiver(bID, (ctx, intent) => { + EvtBus.$emit(bID, intent.getStringExtra("action")); + Application.android.unregisterBroadcastReceiver(bID); + }); + } else { + Application.android.unregisterBroadcastReceiver(bID); + Application.android.registerBroadcastReceiver(bID, (ctx, intent) => { + if (intent.getStringExtra("action") == "dismissAll") { + firedTimers.forEach((t) => this.removeTimer(t.id, 1)); + Application.android.unregisterBroadcastReceiver(bID); + } + }); + } + }, + openReminder() { + this.clearTimerInterval(); + this.$showModal(TimerReminder, { + fullscreen: true, + props: { + formattedTime: this.formattedTime, + removeTimer: this.removeTimer, + togglePause: this.togglePause, + timerAlert: this.timerAlert, + showToast: this.showToast, + }, + }).then(() => { + this.clearTimerInterval(); + this.key = Math.floor(Math.random() * 900) + 100; }); - firingTimers.push(timer); - // if (firingTimers.length == 1) { - // this.$showModal(TimerReminder, { - // fullscreen: true, - // props: { - // timers: firingTimers, - // stop: this.stopFiringTimers, - // formattedTime: this.formattedTime, - // }, - // }); - // } }, - stopFiringTimers() { - firingTimers.forEach((e) => utils.TimerNotif.clear(e.id)); - firingTimers = []; - }, - openReminder() {}, - foregroundService(bool) { + foregroundService(n) { const ctx = Utils.ad.getApplicationContext(); const intent = new android.content.Intent( ctx, com.tns.ForegroundService.class ); - if (bool) + if (n && !this.FGService) { parseInt(Device.sdkVersion) < 26 ? ctx.startService(intent) : ctx.startForegroundService(intent); - else ctx.stopService(intent); + this.setFGService(1); + setNumber("FGService", 1); + } else if (!this.activeTimers.length) { + ctx.stopService(intent); + this.setFGService(0); + setNumber("FGService", 0); + } }, // DATA HANDLERS addTimer() { - this.foregroundService(true); this.$showModal(TimePickerHMS, { props: { title: "ntmr", @@ -258,7 +311,7 @@ export default { ); this.$showModal(Action, { props: { - title: "tmrPrsts", + title: "prsts", list, }, }).then((preset) => { @@ -266,128 +319,155 @@ export default { let timer = JSON.parse( JSON.stringify(this.timerPresets[list.indexOf(preset)]) ); - timer.id = this.getRandomID(); + timer.id = utils.getRandomID(1); + timer.recipeID = this.recipeID; + timer.timerInt = timer.isPaused = 0; + timer.preset = timer.mode = 1; this.addActiveTimer({ timer, - index: this.activeTimers.length, + i: this.activeTimers.length, }); - this.notifyTimers(); + this.timerInfo(); } }); } else { - if (res.time != "00:00:00") { - this.addActiveTimer({ - timer: { - id: this.getRandomID(), - label: res.label, - recipeID: this.recipeID, - time: res.time, - timerInterval: null, - isPaused: false, - preset: 0, - }, - index: this.activeTimers.length, - }); - this.notifyTimers(); - } + let mode = res.time != "00:00:00" ? 1 : 0; + this.addActiveTimer({ + timer: { + id: utils.getRandomID(1), + label: res.label, + recipeID: this.recipeID, + time: res.time, + timerInt: 0, + isPaused: 0, + preset: 0, + done: 0, + mode, + }, + i: this.activeTimers.length, + }); + this.timerInfo(); } } }); }, - removeTimer(id, index, noUndo) { - let temp = this.activeTimers[index]; - this.removeActiveTimer(index); - utils.TimerNotif.clear(id); - if (!noUndo) { - this.showUndoBar("tmrClr") - .then(() => { - this.foregroundService(true); - this.addActiveTimer({ - timer: temp, - index, - }); - this.notifyTimers(); - }) - .catch(() => { - ApplicationSettings.remove(`${temp.id}progress`); - }); + removeTimer(id, noUndo) { + let i = this.activeTimers.findIndex((e) => e.id == id); + let temp = this.activeTimers[i]; + clearInterval(temp.timerInt); + temp.timerInt = 0; + this.removeActiveTimer(i); + let secs = [getNumber(`${temp.id}c`, 0), getNumber(`${temp.id}d`, 0)]; + function removeSettings() { + remove(`${temp.id}c`); + remove(`${temp.id}d`); } - this.notifyTimers(); + removeSettings(); + if (!noUndo) { + this.showUndoBar("tmrRm") + .then(() => { + setNumber(`${temp.id}c`, secs[0]), + setNumber(`${temp.id}d`, secs[1]), + this.addActiveTimer({ + timer: temp, + i, + }); + this.timerInfo(); + }) + .catch(() => removeSettings()); + } + this.timerAlert(); + this.timerInfo(); }, - addToPreset(timer) { - timer = JSON.parse(JSON.stringify(timer)); - timer.recipeID = timer.timerInterval = null; - timer.preset = 1; - this.addTimerPreset(timer); - this.showToast("aTPrst"); - }, - togglePause(timer, bool) { - if (typeof bool === "boolean") timer.isPaused = bool; - else timer.isPaused = !timer.isPaused; + togglePause(timer, n) { + timer.isPaused = + typeof n === "number" ? n : (!timer.isPaused as boolean | 0); this.updateActiveTimer(timer); - this.notifyTimers(); + n ? 0 : this.timerInfo(); }, showToast(data) { - this.toast = localize(data); - utils.timer(5, (val) => { - if (!val) this.toast = val; + this.animateBar(this.snackbar, 0); + this.animateBar(this.appbar, 0).then(() => { + this.showUndo = 0; + this.toast = localize(data); + this.animateBar(this.toastbar, 1); + let a = 5; + clearInterval(barTimer); + barTimer = setInterval(() => a-- < 1 && this.hideBar(), 1000); }); }, showUndoBar(message) { return new Promise((resolve, reject) => { - clearTimeout(undoTimer); - this.showUndo = true; - this.snackMsg = message; - this.countdown = 5; - let a = 5; - undoTimer = setInterval(() => { - if (this.undo) { - this.showUndo = this.undo = false; - clearTimeout(undoTimer); - resolve(true); - } - this.countdown = Math.round((a -= 0.1)); - if (this.countdown < 1) { - this.showUndo = false; - clearTimeout(undoTimer); - reject(true); - } - }, 100); + this.animateBar(this.toastbar, 0); + this.animateBar(this.appbar, 0).then(() => { + this.toast = null; + this.showUndo = 1; + this.snackMsg = message; + this.countdown = 5; + this.animateBar(this.snackbar, 1).then(() => { + let a = 5; + clearInterval(barTimer); + barTimer = setInterval(() => { + if (this.undo) { + this.hideBar(); + resolve(1); + } + this.countdown = Math.round((a -= 0.1)); + if (this.countdown < 1) { + this.hideBar(); + reject(1); + } + }, 100); + }); + }); }); }, - hideBar({ object }) { - this.appbar.translateY = 64; - object - .animate({ - opacity: 0, - translate: { x: 0, y: 64 }, - duration: 250, - curve: CoreTypes.AnimationCurve.ease, - }) - .then(() => { - this.showUndo = false; + hideBar() { + clearInterval(barTimer); + this.animateBar(this.toast ? this.toastbar : this.snackbar, 0).then( + () => { + this.showUndo = this.undo = 0; this.toast = null; - this.appbar.animate({ - translate: { x: 0, y: 0 }, - duration: 250, - curve: CoreTypes.AnimationCurve.ease, - }); - object.opacity = 1; - object.translateY = 0; - clearTimeout(undoTimer); - }); + this.animateBar(this.appbar, 1); + } + ); }, undoDel() { - this.undo = true; + this.undo = 1; + }, + + //NAVIGATION HANDLERS + navigateTo() { + this.$navigateTo(CTSettings, { + transition: { + name: this.RTL ? "slideRight" : "slide", + duration: 200, + curve: "easeOut", + }, + }); + }, + navigateBack() { + setNumber("isTimer", 0); + this.hasBackStack + ? this.$navigateBack() + : this.$navigateTo(EnRecipes, { + clearHistory: true, + }); }, // HELPERS - getRandomID() { - return Math.floor(Math.random() * 9000000000) + 1000000000; + keepScreenOnCountUp() { + utils.keepScreenOn( + this.activeTimers.filter((e: any) => !e.isPaused).length + ); }, }, created() { this.clearTimerInterval(); + this.recipeID && this.addTimer(); + }, + destroyed() { + setNumber("isTimer", 0); }, }; diff --git a/app/components/EditRecipe.vue b/app/components/EditRecipe.vue index 516b4c76..0a54e8fd 100644 --- a/app/components/EditRecipe.vue +++ b/app/components/EditRecipe.vue @@ -1,15 +1,11 @@ @@ -294,7 +307,6 @@ diff --git a/app/components/GroceryList.vue b/app/components/GroceryList.vue deleted file mode 100644 index 83dab81f..00000000 --- a/app/components/GroceryList.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/app/components/MealPlanner.vue b/app/components/MealPlanner.vue index 179e54d2..e37bca00 100644 --- a/app/components/MealPlanner.vue +++ b/app/components/MealPlanner.vue @@ -1,112 +1,146 @@ - diff --git a/app/components/ViewRecipe.vue b/app/components/ViewRecipe.vue index 5742d458..c89ae113 100644 --- a/app/components/ViewRecipe.vue +++ b/app/components/ViewRecipe.vue @@ -1,16 +1,21 @@ @@ -255,13 +278,13 @@ import { Utils, Span, FormattedString, - Label, Observable, Screen, CoreTypes, + WebView, } from "@nativescript/core"; +import { RLabel } from "~/rtl-ui"; import { localize } from "@nativescript/localize"; -const intl = require("nativescript-intl"); import { mapActions, mapState } from "vuex"; import CookingTimer from "./CookingTimer.vue"; import EditRecipe from "./EditRecipe.vue"; @@ -269,17 +292,24 @@ import Action from "./modals/Action.vue"; import Toast from "./sub/Toast.vue"; import Prompt from "./modals/Prompt.vue"; import * as utils from "~/shared/utils"; +const Intl = require("nativescript-intl"); +let barTimer; +declare const android: any; + export default { components: { Toast }, - props: ["filterTrylater", "recipeID"], + props: ["filterTrylater", "recipeID", "yieldQuantity"], data() { return { - busy: false, + busyEdit: 0, + busyDup: 0, yieldMultiplier: 1, recipe: null, currentRecipeID: this.recipeID, scrollview: null, appbar: null, + sidebar: null, + toastbar: null, ingcon: null, inscon: null, cmbcon: null, @@ -290,13 +320,15 @@ export default { checked: 0, stepsDid: 0, toast: null, - photoOpen: false, - showTitleArr: [false, false, false, false], + photoOpen: 0, + showTitleArr: [0, 0, 0, 0], sticky: null, + view: null, + wv: null, }; }, computed: { - ...mapState(["icon", "recipes"]), + ...mapState(["icon", "recipes", "RTL"]), tempYieldQuantity() { return Math.abs(this.yieldMultiplier) > 0 ? Math.abs(parseFloat(this.yieldMultiplier)) @@ -320,29 +352,38 @@ export default { } return val; }, + hasPrinterSupport() { + return utils.Printer.isSupported(); + }, }, methods: { - ...mapActions([ - "toggleStateAction", - "setComponent", - "setRatingAction", - "toggleCartAction", - ]), - onPageLoad({ object }) { + ...mapActions(["toggleStateAction", "setRatingAction", "toggleCartAction"]), + pgLoad({ object }) { + this.busyDup = this.busyEdit = this.photoOpen = 0; object.bindingContext = new Observable(); - this.busy = this.photoOpen = false; - this.setComponent("ViewRecipe"); if (this.yieldMultiplier == this.recipe.yieldQuantity) this.yieldMultiplier = this.recipe.yieldQuantity; - this.keepScreenOn(true); + utils.keepScreenOn(1); this.syncCombinations(); + this.view = object.page.getViewById("printview"); }, onPageUnload() { - this.keepScreenOn(false); + utils.keepScreenOn(0); }, - onAppBarLoad({ object }) { + sbload({ object }) { + this.sidebar = object; + }, + abLoad({ object }) { this.appbar = object; }, + tbLoad({ object }) { + this.toastbar = object; + this.recipe.tried && this.recipe.lastTried && this.showLastTried(); + }, + wvLoad({ object }) { + this.wv = object; + utils.updateLocale(); + }, onIngsLoad({ object }) { this.ingcon = object; }, @@ -366,24 +407,22 @@ export default { this.imgView = object; this.imgView.visibility = "collapsed"; this.imgView.top = 24; - this.imgView.left = Screen.mainScreen.widthDIPs - 112; + this.imgView.left = this.RTL ? 16 : Screen.mainScreen.widthDIPs - 112; }, onStickyLoad({ object }) { this.sticky = object; }, - fixTitle({ object }, swipeUp: boolean): void { + fixTitle(object, swipeUp: boolean): void { let ingL = this.recipe.ingredients.length; let insL = this.recipe.instructions.length; let cmbL = this.recipe.combinations.length; let notL = this.recipe.notes.length; - const isTop = (label): boolean => { let pos = label.getLocationRelativeTo(object).y; return label === this.cmbcon || label === this.notesT ? pos < 0 : pos + 32 < 0; }; - const setVisibleTitle = (n: number): void => { let arr = [ingL, insL, cmbL, notL]; this.showTitleArr = Array.from( @@ -391,7 +430,6 @@ export default { (v, i) => (v = arr[i] ? i < n : false) ); }; - if (swipeUp) { if (ingL && !this.showTitleArr[0] && isTop(this.ingcon)) setVisibleTitle(1); @@ -412,45 +450,71 @@ export default { setVisibleTitle(0); } }, - onScroll(args) { + svScroll({ object, scrollY }) { let swipeUp: boolean; - let y = args.scrollY; + let y = scrollY; if (y) { swipeUp = y > this.scrollPos; this.scrollPos = Math.abs(y); - this.fixTitle(args, swipeUp); + this.fixTitle(object, swipeUp); if (!this.toast) { let ab = this.appbar.translateY; - if (swipeUp && ab == 0) - this.appbar.animate({ - translate: { x: 0, y: 64 }, - duration: 250, - curve: CoreTypes.AnimationCurve.ease, - }); - else if (!swipeUp && ab == 64) - this.appbar.animate({ - translate: { x: 0, y: 0 }, - duration: 250, - curve: CoreTypes.AnimationCurve.ease, - }); + if (swipeUp && ab == 0) this.hideBars(); + else if (!swipeUp && ab == 64) this.showBars(); } } }, - // HELPERS + showBars() { + this.appbar.animate({ + translate: { x: 0, y: 0 }, + duration: 200, + curve: CoreTypes.AnimationCurve.ease, + }); + this.sidebar.animate({ + translate: { x: 0, y: 0 }, + duration: 200, + curve: CoreTypes.AnimationCurve.ease, + }); + }, + hideBars() { + this.appbar.animate({ + translate: { x: 0, y: 64 }, + duration: 200, + curve: CoreTypes.AnimationCurve.ease, + }); + this.sidebar.animate({ + translate: { x: this.RTL ? -64 : 64, y: 0 }, + duration: 200, + curve: CoreTypes.AnimationCurve.ease, + }); + }, + + // Helpers getTitleCount(title, type) { - let count = this.recipe[type].length; - let selected = null; + let c = this.recipe[type].length; + let s = null; switch (title) { case "ings": - selected = this.checked; + s = this.checked; break; case "inss": - selected = this.stepsDid; + s = this.stepsDid; break; } - let text = selected ? ` (${selected}/${count})` : ` (${count})`; + c = this.getLocaleN(c); + s = s && this.getLocaleN(s); + let text = s ? ` (${s}/${c})` : ` (${c})`; return localize(title) + text; }, + getIngredientItem(item) { + return `${ + this.roundedQuantity(item.quantity) + ? this.roundedQuantity(item.quantity) + " " + : "" + }${this.roundedQuantity(item.quantity) ? localize(item.unit) + " " : ""}${ + item.item + }`; + }, changeYield() { this.$showModal(Prompt, { props: { @@ -485,31 +549,25 @@ export default { ); }, showLastTried() { - this.toast = localize("triedInfo", this.niceDate(this.recipe.lastTried)); - utils.timer(10, (val) => { - if (!val) this.toast = val; + this.animateBar(this.appbar, 0).then(() => { + this.toast = localize( + "triedInfo", + this.niceDate(this.recipe.lastTried) + ); + this.animateBar(this.toastbar, 1); + let a = 10; + clearInterval(barTimer); + barTimer = setInterval(() => a-- < 1 && this.hideBar(), 1000); }); }, - hideLastTried({ object }) { - object - .animate({ - opacity: 0, - translate: { x: 0, y: 64 }, - duration: 250, - curve: CoreTypes.AnimationCurve.ease, - }) - .then(() => { - this.showUndo = false; - this.appbar.translateY = 64; - this.appbar.animate({ - translate: { x: 0, y: 0 }, - duration: 250, - curve: CoreTypes.AnimationCurve.ease, - }); - object.opacity = 1; - object.translateY = 0; - this.toast = null; - }); + hideBar() { + clearInterval(barTimer); + this.animateBar(this.toastbar, 0).then(() => { + this.toast = null; + this.photoOpen + ? (this.appbar.opacity = 1) + : this.animateBar(this.appbar, 1); + }); }, // getMeasure(value: number, unit: string) { // let vm = this; @@ -560,14 +618,6 @@ export default { ) / 100 ); }, - keepScreenOn(boolean) { - let activity = - Application.android.foregroundActivity || - Application.android.startActivity; - let window = activity.getWindow(); - let flag = android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; - boolean ? window.addFlags(flag) : window.clearFlags(flag); - }, formattedTime(time) { let t = time.split(":"); let h = parseInt(t[0]); @@ -577,15 +627,13 @@ export default { return h ? (m ? `${h} ${hr} ${m} ${min}` : `${h} ${hr}`) : `${m} ${min}`; }, formattedDate(date) { - let d = new Date(date); - var dateFormat = new intl.DateTimeFormat(null, { + return new Intl.DateTimeFormat(null, { year: "numeric", month: "long", day: "numeric", hour: "numeric", minute: "numeric", - }).format(d); - return `${dateFormat}`; + }).format(new Date(date)); }, isValidURL(string) { let pattern = new RegExp("^https?|^www", "ig"); @@ -654,26 +702,38 @@ export default { this.inscon.getChildAt(i).className = "instruction"; } }, + getDates() { + let u = `${localize("Last updated")}: ${this.formattedDate( + this.recipe.lastModified + )}`; + let c = `${localize("Created")}: ${this.formattedDate( + this.recipe.created + )}`; + return { + u, + c, + uc: u + "\n" + c, + }; + }, - // NAVIGATION HANDLERS + // NavigationHandlers editRecipe() { - this.busy = true; + this.busyEdit = 1; this.$navigateTo(EditRecipe, { props: { - navigationFromView: true, filterTrylater: this.filterTrylater, recipeID: this.currentRecipeID, }, - // backstackVisible: false, + animated: false, }); }, viewCombination(combination) { this.scrollview.scrollToVerticalOffset(0, true); this.recipe = this.recipes.filter((e) => e.id === combination)[0]; - this.showTitleArr = new Array(4).fill(false); + this.showTitleArr = new Array(4).fill(0); this.clearChecks(); this.clearSteps(); - this.recipe.ingredients.forEach(() => this.checks.push(false)); + this.recipe.ingredients.forEach(() => this.checks.push(0)); this.currentRecipeID = combination; this.syncCombinations(); this.createNotes(); @@ -681,7 +741,7 @@ export default { this.recipe.tried && this.recipe.lastTried && this.showLastTried(); }, - // SHARE ACTION + // ShareAction shareHandler() { if (this.recipe.image) { this.$showModal(Action, { @@ -696,7 +756,7 @@ export default { break; case "pht": ImageSource.fromFile(this.recipe.image).then((res) => - utils.shareImage(res, localize("srpu")) + utils.shareImage(res, localize("srpu"), this.recipe.title) ); break; } @@ -769,7 +829,7 @@ export default { utils.shareText(shareContent, localize("sru")); }, - // DATA HANDLERS + // DataHandlers toggle(key: string, setDate: boolean) { this.toggleStateAction({ id: this.currentRecipeID, @@ -787,7 +847,7 @@ export default { } }, - // SHOPPINGLIST + // ShoppingList toggleCart() { if (!this.recipe.inBag) { } else { @@ -797,10 +857,10 @@ export default { }); }, - // NOTES + // Notes createNote(note) { let regex = /(https?:\/\/[^\s]+)/g; - const lbl = new Label(); + const lbl = new RLabel(); lbl.className = "note"; lbl.textWrap = true; let fString = new FormattedString(); @@ -851,7 +911,8 @@ export default { } else this.$navigateBack(); }, viewPhoto() { - this.photoOpen = true; + this.hideBars(); + this.photoOpen = 1; this.hijackBackEvent(); let pv = this.imgView; pv.visibility = "visible"; @@ -865,16 +926,16 @@ export default { pv.animate({ width: sw, height: sw, - translate: { x: 112 - sw, y: (sh - sw) / 3 }, - duration: 250, + translate: { x: this.RTL ? -16 : 112 - sw, y: (sh - sw) / 3 }, + duration: 200, curve: CoreTypes.AnimationCurve.ease, }) ) .then(() => pv.animate({ height: sh, - translate: { x: -sw + 112, y: -((sh - sw) / 6) }, - duration: 250, + translate: { x: this.RTL ? -16 : 112 - sw, y: -((sh - sw) / 6) }, + duration: 200, curve: CoreTypes.AnimationCurve.ease, }) ); @@ -886,8 +947,8 @@ export default { pv.animate({ width: sw, height: sw, - translate: { x: 112 - sw, y: (sh - sw) / 3 }, - duration: 250, + translate: { x: this.RTL ? -16 : 112 - sw, y: (sh - sw) / 3 }, + duration: 200, curve: CoreTypes.AnimationCurve.ease, }) .then(() => @@ -895,7 +956,7 @@ export default { width: 96, height: 96, translate: { x: 0, y: 0 }, - duration: 250, + duration: 200, curve: CoreTypes.AnimationCurve.ease, }) ) @@ -907,37 +968,162 @@ export default { ) .then(() => { pv.visibility = "collapsed"; - this.photoOpen = false; + this.photoOpen = 0; this.releaseBackEvent(); + this.showBars(); }); }, - navigateBack() { - this.photoOpen ? this.closePhoto() : this.$navigateBack(); - }, - //TIMERS + // Timers openCookingTimer() { this.$navigateTo(CookingTimer, { props: { recipeID: this.recipe.id, }, + animated: false, }); }, - //HELPERS + + //DuplicateRecipe + duplicateRecipe() { + this.busyDup = 1; + let dupRecipe = Object.assign({}, this.recipe); + dupRecipe.id = utils.getRandomID(0); + dupRecipe.title = dupRecipe.title + " " + localize("cpy"); + this.$navigateTo(EditRecipe, { + props: { + dupRecipe, + }, + animated: false, + }); + }, + + // Print + prepareHTML() { + let r = this.recipe; + const head = `EnRecipes - Recipe for Print`; + const getStarRating = () => { + let rate = ``; + let unrate = ``; + return rate.repeat(r.rating) + unrate.repeat(5 - r.rating); + }; + const img = r.image ? `${r.title}` : ""; + const getIngs = () => { + let ing = []; + r.ingredients.forEach((e) => { + ing.push(`

  • ${this.getIngredientItem(e)}
  • `); + }); + return ing.join(""); + }; + const getIns = () => { + let ins = []; + r.instructions.forEach((e) => { + ins.push(`
  • ${e}
  • `); + }); + return ins.join(""); + }; + const getCmbs = () => { + let cmb = []; + r.combinations.forEach((e) => { + cmb.push(`

    ${this.getCombinationTitle(e)}

    `); + }); + return cmb.join(""); + }; + const getNotes = () => { + let regex = /(https?:\/\/[^\s]+)/g; + let n = []; + const createSpan = (val, isUrl) => { + return isUrl + ? `${val}` + : val; + }; + r.notes.forEach((e) => { + let arr = e.split(regex); + let single = []; + arr.forEach((f) => { + single.push(createSpan(f, regex.test(f))); + }); + n.push(`

    ${single.join("")}

    `); + }); + return n.join(""); + }; + return `${head}

    ${localize( + "cui" + )}

    ${r.cuisine}

    ${localize("cat")}

    ${ + r.category + }

    ${ + r.tags.length + ? `

    ${localize( + "ts" + )}

    ${this.getTags(r.tags)}

    ` + : "" + } ${ + this.hasTime(r.prepTime) || this.hasTime(r.cookTime) + ? `
    ${ + this.hasTime(r.prepTime) + ? `

    ${localize("prepT")}

    ${this.formattedTime( + r.prepTime + )}

    ` + : "" + } ${ + this.hasTime(r.cookTime) + ? `

    ${localize("cookT")}

    ${this.formattedTime( + r.cookTime + )}

    ` + : "" + }
    ` + : "" + }

    ${localize("yld")}

    ${ + this.tempYieldQuantity + } ${localize(r.yieldUnit)}

    ${localize( + "Difficulty level" + )}

    ${r.difficulty}

    ${ + r.ingredients.length + ? `

    ${this.getTitleCount("ings", "ingredients")}

    ` + : "" + }${ + r.instructions.length + ? `

    ${this.getTitleCount("inss", "instructions")}

    ` + : "" + }
      ${getIns()}
    ${ + r.combinations.length + ? `

    ${this.getTitleCount("cmbs", "combinations")}

    ` + : "" + } ${getCmbs()} ${ + r.notes.length ? `

    ${this.getTitleCount("nos", "notes")}

    ` : "" + } ${getNotes()}

    ${this.getDates().u}

    ${ + this.getDates().c + }

    + +`; + }, + printView() { + let wv = this.wv as WebView; + const fileName = `${this.recipe.title} - ${localize("EnRecipes")}`; + wv.src = this.prepareHTML(); + wv.once("loadFinished", () => + utils.Printer.print(wv, fileName).then(() => (wv.src = null)) + ); + }, + + // Helpers touchYield({ object, action }) { object.className = action.match(/down|move/) - ? "value clickable fade" - : "value clickable"; + ? "value accent fade" + : "value accent"; if (action == "up") this.changeYield(); }, }, created() { this.recipe = this.recipes.filter((e) => e.id === this.currentRecipeID)[0]; - this.recipe.ingredients.forEach((e) => this.checks.push(false)); + this.recipe.ingredients.forEach((e) => this.checks.push(0)); }, mounted() { - this.yieldMultiplier = this.recipe.yieldQuantity; - this.recipe.tried && this.recipe.lastTried && this.showLastTried(); + this.yieldMultiplier = this.yieldQuantity || this.recipe.yieldQuantity; }, }; diff --git a/app/components/modals/Action.vue b/app/components/modals/Action.vue index 5ec66390..b935cd09 100644 --- a/app/components/modals/Action.vue +++ b/app/components/modals/Action.vue @@ -2,14 +2,14 @@ -