diff --git a/config.xml b/config.xml index ce93948c4..cbc79bf95 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + Beanconqueror Lars Saalbach diff --git a/package-lock.json b/package-lock.json index 280f85555..7ba5d102f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,8 @@ "@ionic/cordova-builders": "^8.0.0", "@ionic/storage": "^4.0.0", "@ionic/storage-angular": "^4.0.0", + "@meticulous-home/espresso-api": "^0.3.1", + "@meticulous-home/espresso-profile": "^0.4.0", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^4.0.0", "@zip.js/zip.js": "^2.7.14", @@ -61,7 +63,6 @@ "lodash": "^4.17.21", "long": "^5.2.0", "luxon": "^2.3.0", - "meticulous-api": "github:FFFuego/meticulous-typescript-api#dist", "moment": "^2.29.4", "ngx-stars": "^1.6.4", "postcss": "^8.3.5", @@ -91,7 +92,7 @@ "@wisdomgarden/cordova-plugin-filepath": "git+https://github.com/wisdom-garden/cordova-plugin-filepath.git", "cordova-android": "^13.0.0", "cordova-clipboard": "^1.3.0", - "cordova-ios": "^7.1.0", + "cordova-ios": "^7.1.1", "cordova-plugin-add-swift-support": "^2.0.2", "cordova-plugin-advanced-http": "^3.3.1", "cordova-plugin-android-permissions": "^1.1.5", @@ -6294,6 +6295,23 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, + "node_modules/@meticulous-home/espresso-api": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@meticulous-home/espresso-api/-/espresso-api-0.3.1.tgz", + "integrity": "sha512-KeZuK0RDDc9HhMNxyYj0BRfEgGvNOUKu5zyn8RYI4ezUCnn61PP4FbykiB7YxzBvsr8PT+5BOPyXSvqkDaC1FA==", + "license": "GPLv3", + "dependencies": { + "axios": "^1.6.8", + "meticulous-typescript-profile": "github:MeticulousHome/meticulous-typescript-profile#dist", + "socket.io-client": "^4.7.5" + } + }, + "node_modules/@meticulous-home/espresso-profile": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@meticulous-home/espresso-profile/-/espresso-profile-0.4.0.tgz", + "integrity": "sha512-jHG2y6F6mi5YTFE0nmyc5NKUGmyDxorLiix1h8CB5ch/Plae+fvD7RsxetA6nh3lAn8Zfz9U9am2kolokY0Zyg==", + "license": "GPL-3.0" + }, "node_modules/@netflix/nerror": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@netflix/nerror/-/nerror-1.1.3.tgz", @@ -9262,10 +9280,11 @@ } }, "node_modules/cordova-ios": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cordova-ios/-/cordova-ios-7.1.0.tgz", - "integrity": "sha512-9/vPU+GWRdfxNIkAc9Gq6yejMIgpy59ycP8WyVJJ7HfuPSTBUQT8AS2h5ZJOeN4Y/URyEwxQCrAqSADDBVCESA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/cordova-ios/-/cordova-ios-7.1.1.tgz", + "integrity": "sha512-JwTyPxWcAZlbIOR5QO6TaJzkoSzfrp7jrlX01bWZ7Sxp0PYXejAJbA6J0W4u11M+atrQRNimNltZDyAlSBW2tw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "cordova-common": "^5.0.0", "elementtree": "^0.1.7", @@ -9286,6 +9305,7 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } @@ -9295,6 +9315,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -12750,6 +12771,7 @@ "resolved": "https://registry.npmjs.org/ios-sim/-/ios-sim-8.0.2.tgz", "integrity": "sha512-P7nEG771bfd+JoMRjnis1gpZOkjTUUxu+4Ek1Z+eoaEEoT9byllU9pxfQ8Df7hL3gSkIQxNwTSLhos2I8tWUQA==", "dev": true, + "license": "MIT", "dependencies": { "bplist-parser": "^0.0.6", "nopt": "1.0.9", @@ -12767,13 +12789,15 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.0.6.tgz", "integrity": "sha512-fGeghPEH4Eytvf+Mi446aKcDqvkA/+eh6r7QGiZWMQG6TzqrnsToLP379XFfqRSZ41+676hhGIm++maNST1Apw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ios-sim/node_modules/nopt": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.9.tgz", "integrity": "sha512-CmUZ3rzN0/4kRHum5pGRiGkhmBMzgtEDxrZVHqRJDSv8qK6s+wzaig/xeyB22Due5aZQeTiEZg/nrmMH2tapDQ==", "dev": true, + "license": "MIT", "dependencies": { "abbrev": "1" }, @@ -15075,16 +15099,6 @@ "node": ">= 0.6" } }, - "node_modules/meticulous-api": { - "version": "0.0.2", - "resolved": "git+ssh://git@github.com/FFFuego/meticulous-typescript-api.git#c1ae8038e22db7ff9547f88038b9566b4f1f9718", - "license": "GPLv3", - "dependencies": { - "axios": "^1.6.8", - "meticulous-typescript-profile": "github:MeticulousHome/meticulous-typescript-profile#dist", - "socket.io-client": "^4.7.5" - } - }, "node_modules/meticulous-typescript-profile": { "version": "0.1.0", "resolved": "git+ssh://git@github.com/MeticulousHome/meticulous-typescript-profile.git#c4520f2238770d8432272e2fbc5d29f90a78720d", @@ -18915,6 +18929,7 @@ "resolved": "https://registry.npmjs.org/simctl/-/simctl-2.0.3.tgz", "integrity": "sha512-kKCak0yszxHae5eVWcmrjV3ouUGac3sjlhjdLWpyPu4eiQcWoHsCrqS34kkgzHN8Ystqkh/LFjzrldk/g3BYJg==", "dev": true, + "license": "MIT", "dependencies": { "shelljs": "^0.8.5", "tail": "^0.4.0" @@ -21824,6 +21839,7 @@ "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" @@ -21837,6 +21853,7 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", "dev": true, + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } @@ -21865,7 +21882,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/xml-escape/-/xml-escape-1.1.0.tgz", "integrity": "sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg==", - "dev": true + "dev": true, + "license": "MIT License" }, "node_modules/xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index 316b14aff..457cd34b9 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,8 @@ "@ionic/cordova-builders": "^8.0.0", "@ionic/storage": "^4.0.0", "@ionic/storage-angular": "^4.0.0", + "@meticulous-home/espresso-api": "^0.3.1", + "@meticulous-home/espresso-profile": "^0.4.0", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^4.0.0", "@zip.js/zip.js": "^2.7.14", @@ -68,7 +70,6 @@ "lodash": "^4.17.21", "long": "^5.2.0", "luxon": "^2.3.0", - "meticulous-api": "github:FFFuego/meticulous-typescript-api#dist", "moment": "^2.29.4", "ngx-stars": "^1.6.4", "postcss": "^8.3.5", @@ -98,7 +99,7 @@ "@wisdomgarden/cordova-plugin-filepath": "git+https://github.com/wisdom-garden/cordova-plugin-filepath.git", "cordova-android": "^13.0.0", "cordova-clipboard": "^1.3.0", - "cordova-ios": "^7.1.0", + "cordova-ios": "^7.1.1", "cordova-plugin-add-swift-support": "^2.0.2", "cordova-plugin-advanced-http": "^3.3.1", "cordova-plugin-android-permissions": "^1.1.5", @@ -210,22 +211,22 @@ "cordova-plugin-nativestorage": {}, "cordova-plugin-androidx": {}, "cordova-plugin-androidx-adapter": {}, - "cordova-plugin-ble-central": { - "BLUETOOTH_USAGE_DESCRIPTION": "Bluetooth access needed to connect smartscales", - "IOS_INIT_ON_LOAD": "false", - "ACCESS_BACKGROUND_LOCATION": "false", - "BLUETOOTH_RESTORE_STATE": "false" - }, "cordova-plugin-inappbrowser": {}, "skwas-cordova-plugin-datetimepicker": {}, "cordova-plugin-statusbar": {}, "cordova-plugin-file": { "ANDROIDX_WEBKIT_VERSION": "1.4.0" + }, + "cordova-plugin-ble-central": { + "BLUETOOTH_USAGE_DESCRIPTION": "Bluetooth access needed to connect smartscales", + "IOS_INIT_ON_LOAD": "false", + "ACCESS_BACKGROUND_LOCATION": "false", + "BLUETOOTH_RESTORE_STATE": "false" } }, "platforms": [ - "ios", - "android" + "android", + "ios" ] }, "platforms": [ @@ -240,4 +241,4 @@ "*.css": "stylelint --fix", "*.{ts,js,css,md}": "prettier --write" } -} +} \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5c9e57620..f06ed532e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -506,6 +506,8 @@ export class AppComponent implements AfterViewInit { settingLanguage = 'fr'; } else if (systemLanguage === 'id') { settingLanguage = 'id'; + } else if (systemLanguage === 'nl') { + settingLanguage = 'nl'; } else { settingLanguage = 'en'; } diff --git a/src/app/app.scss b/src/app/app.scss index 5888a116d..e760198ff 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -739,6 +739,7 @@ ion-menu:host .menu-inner { font-weight: bold; } + .button-top-absolute { position: absolute; top: -20px; @@ -943,24 +944,27 @@ swiper-container { --swiper-scrollbar-drag-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.5); } -swiper-slide { - display: flex; - position: relative; +swiper-container.swiper { + swiper-slide { + display: flex; + position: relative; - flex-direction: column; - flex-shrink: 0; - align-items: center; - justify-content: center; + flex-direction: column; + flex-shrink: 0; + align-items: center; + justify-content: center; - width: 100%; - height: 100%; + width: 100%; + height: 100%; - font-size: 18px; + font-size: 18px; - text-align: center; - box-sizing: border-box; + text-align: center; + box-sizing: border-box; + } } + swiper-slide img { width: auto; max-width: 100%; diff --git a/src/app/beans/bean-modal-select/bean-modal-select.component.html b/src/app/beans/bean-modal-select/bean-modal-select.component.html index 28499557f..45fa1b392 100644 --- a/src/app/beans/bean-modal-select/bean-modal-select.component.html +++ b/src/app/beans/bean-modal-select/bean-modal-select.component.html @@ -101,6 +101,11 @@
+
+ +
+
@@ -178,6 +183,11 @@
+
+ +
+ @@ -260,6 +270,11 @@
( +
+ +
+
@@ -348,7 +363,11 @@
( +
+ +
+
@@ -430,6 +449,11 @@
+
+ +
+
@@ -511,7 +535,11 @@
( +
+ +
+
diff --git a/src/app/beans/bean-modal-select/bean-modal-select.component.ts b/src/app/beans/bean-modal-select/bean-modal-select.component.ts index a862ad998..f04058aee 100644 --- a/src/app/beans/bean-modal-select/bean-modal-select.component.ts +++ b/src/app/beans/bean-modal-select/bean-modal-select.component.ts @@ -409,7 +409,8 @@ export class BeanModalSelectComponent implements OnInit { e.name?.toLowerCase().includes(searchText) || e.roaster?.toLowerCase().includes(searchText) || e.aromatics?.toLowerCase().includes(searchText) || - e.frozenId?.toLowerCase().includes(searchText) + e.frozenId?.toLowerCase().includes(searchText) || + e.ean_article_number?.toLowerCase().includes(searchText) ); } if (_type === 'open') { diff --git a/src/app/beans/bean-popover-freeze/bean-popover-freeze.component.html b/src/app/beans/bean-popover-freeze/bean-popover-freeze.component.html index 3d977ecbc..773cd6f8a 100644 --- a/src/app/beans/bean-popover-freeze/bean-popover-freeze.component.html +++ b/src/app/beans/bean-popover-freeze/bean-popover-freeze.component.html @@ -43,11 +43,11 @@ prevent-characters remove-empty-number spellcheck="false" type="text" tabIndex="2"> - - diff --git a/src/app/beans/bean-popover-freeze/bean-popover-freeze.component.ts b/src/app/beans/bean-popover-freeze/bean-popover-freeze.component.ts index d7a006e28..8bb2d0fd3 100644 --- a/src/app/beans/bean-popover-freeze/bean-popover-freeze.component.ts +++ b/src/app/beans/bean-popover-freeze/bean-popover-freeze.component.ts @@ -59,7 +59,10 @@ export class BeanPopoverFreezeComponent implements OnInit { public ngOnInit() { // cant be done in constructor, else the bean object is not known - this.leftOverBeanBagWeight = this.bean.weight - this.getUsedWeightCount(); + this.leftOverBeanBagWeight = this.uiHelper.toFixedIfNecessary( + this.bean.weight - this.getUsedWeightCount(), + 1 + ); } public getUsedWeightCount(): number { @@ -86,8 +89,10 @@ export class BeanPopoverFreezeComponent implements OnInit { } public async save() { - const spillOver = - this.leftOverBeanBagWeight - this.getActualFreezingQuantity(); + const spillOver = this.uiHelper.toFixedIfNecessary( + this.leftOverBeanBagWeight - this.getActualFreezingQuantity(), + 1 + ); let index = 1; @@ -109,10 +114,16 @@ export class BeanPopoverFreezeComponent implements OnInit { this.bean.config.uuid ); if (brews.length > 0) { - const oldWeight = this.bean.weight; - this.bean.weight = this.bean.weight - this.getActualFreezingQuantity(); + const oldWeight = this.uiHelper.toFixedIfNecessary(this.bean.weight, 1); + this.bean.weight = this.uiHelper.toFixedIfNecessary( + this.bean.weight - this.getActualFreezingQuantity(), + 1 + ); try { - const newCost = (this.bean.cost * this.bean.weight) / oldWeight; + const newCost = this.uiHelper.toFixedIfNecessary( + (this.bean.cost * this.bean.weight) / oldWeight, + 2 + ); this.bean.cost = newCost; } catch (ex) { this.bean.cost = 0; @@ -148,10 +159,16 @@ export class BeanPopoverFreezeComponent implements OnInit { /** * Because we had already maybe some brews, we take the bean weight, and subtract it with the spill over, because just using the spill over would not take previus brews into account */ - const oldWeight = this.bean.weight; - this.bean.weight = this.bean.weight - this.getActualFreezingQuantity(); + const oldWeight = this.uiHelper.toFixedIfNecessary(this.bean.weight, 1); + this.bean.weight = this.uiHelper.toFixedIfNecessary( + this.bean.weight - this.getActualFreezingQuantity(), + 1 + ); try { - const newCost = (this.bean.cost * this.bean.weight) / oldWeight; + const newCost = this.uiHelper.toFixedIfNecessary( + (this.bean.cost * this.bean.weight) / oldWeight, + 2 + ); this.bean.cost = newCost; } catch (ex) { this.bean.cost = 0; @@ -189,13 +206,16 @@ export class BeanPopoverFreezeComponent implements OnInit { if (this.bean.cost !== 0) { try { - const newCost = (this.bean.cost * _freezingWeight) / this.bean.weight; + const newCost = this.uiHelper.toFixedIfNecessary( + (this.bean.cost * _freezingWeight) / this.bean.weight, + 2 + ); clonedBean.cost = newCost; } catch (ex) { clonedBean.cost = 0; } } - clonedBean.weight = _freezingWeight; + clonedBean.weight = this.uiHelper.toFixedIfNecessary(_freezingWeight, 1); clonedBean.config = new Config(); const newClonedBean = await this.uiBeanStorage.add(clonedBean); const newBean: Bean = new Bean(); @@ -244,18 +264,35 @@ export class BeanPopoverFreezeComponent implements OnInit { return quantity; } + public isAddingBagDisabled() { + if (this.freezePartialBagGrams <= 0) { + return true; + } + if ( + this.uiHelper.toFixedIfNecessary( + Number(this.freezePartialBagGrams) + this.getActualFreezingQuantity(), + 1 + ) > this.leftOverBeanBagWeight + ) { + return true; + } + return false; + } + public addOnePartialBag() { this.addedBags.push({ - weight: this.freezePartialBagGrams, + weight: Number(this.freezePartialBagGrams), type: this.frozenStorage, }); - const leftFreezingCount = - this.leftOverBeanBagWeight - this.getActualFreezingQuantity(); + const leftFreezingCount = this.uiHelper.toFixedIfNecessary( + this.leftOverBeanBagWeight - this.getActualFreezingQuantity(), + 1 + ); if (leftFreezingCount < this.freezePartialBagGrams) { this.freezePartialBagGrams = this.uiHelper.toFixedIfNecessary( leftFreezingCount, - 2 + 1 ); } } @@ -263,16 +300,18 @@ export class BeanPopoverFreezeComponent implements OnInit { public addMaxPartialBags() { while (true) { this.addedBags.push({ - weight: this.freezePartialBagGrams, + weight: Number(this.freezePartialBagGrams), type: this.frozenStorage, }); - const leftFreezingCount = - this.leftOverBeanBagWeight - this.getActualFreezingQuantity(); + const leftFreezingCount = this.uiHelper.toFixedIfNecessary( + this.leftOverBeanBagWeight - this.getActualFreezingQuantity(), + 1 + ); if (leftFreezingCount < this.freezePartialBagGrams) { this.freezePartialBagGrams = this.uiHelper.toFixedIfNecessary( leftFreezingCount, - 2 + 1 ); break; } @@ -281,8 +320,10 @@ export class BeanPopoverFreezeComponent implements OnInit { public deleteBag(_index) { this.addedBags.splice(_index, 1); - const leftFreezingCount = - this.leftOverBeanBagWeight - this.getActualFreezingQuantity(); + const leftFreezingCount = this.uiHelper.toFixedIfNecessary( + this.leftOverBeanBagWeight - this.getActualFreezingQuantity(), + 1 + ); if (leftFreezingCount < this.freezePartialBagGrams) { this.freezePartialBagGrams = leftFreezingCount; } diff --git a/src/app/beans/bean-popover-frozen-list/bean-popover-frozen-list.component.ts b/src/app/beans/bean-popover-frozen-list/bean-popover-frozen-list.component.ts index 29c022e95..ebd9e99e3 100644 --- a/src/app/beans/bean-popover-frozen-list/bean-popover-frozen-list.component.ts +++ b/src/app/beans/bean-popover-frozen-list/bean-popover-frozen-list.component.ts @@ -7,11 +7,9 @@ import { ViewChild, } from '@angular/core'; import { Bean } from '../../../classes/bean/bean'; -import { Brew } from '../../../classes/brew/brew'; import { AgVirtualSrollComponent } from 'ag-virtual-scroll'; import { ModalController } from '@ionic/angular'; import { UIBeanHelper } from '../../../services/uiBeanHelper'; -import { UIBrewHelper } from '../../../services/uiBrewHelper'; @Component({ selector: 'app-bean-popover-frozen-list', diff --git a/src/app/beans/bean-popover-list/bean-popover-list.component.html b/src/app/beans/bean-popover-list/bean-popover-list.component.html new file mode 100644 index 000000000..08d289c2e --- /dev/null +++ b/src/app/beans/bean-popover-list/bean-popover-list.component.html @@ -0,0 +1,19 @@ + + + {{"BEAN_LIST" | translate}} + + + + + + + + + + + + + + + diff --git a/src/app/beans/bean-popover-list/bean-popover-list.component.scss b/src/app/beans/bean-popover-list/bean-popover-list.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/beans/bean-popover-list/bean-popover-list.component.spec.ts b/src/app/beans/bean-popover-list/bean-popover-list.component.spec.ts new file mode 100644 index 000000000..5af36a70d --- /dev/null +++ b/src/app/beans/bean-popover-list/bean-popover-list.component.spec.ts @@ -0,0 +1,32 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { BeanPopoverListComponent } from './bean-popover-list.component'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { TranslateServiceMock } from '../../../classes/mock'; + +describe('BeanPopoverListComponent', () => { + let component: BeanPopoverListComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [BeanPopoverListComponent], + imports: [IonicModule.forRoot(), TranslateModule.forRoot()], + providers: [ + { + provide: TranslateService, + useValue: TranslateServiceMock, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(BeanPopoverListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/beans/bean-popover-list/bean-popover-list.component.ts b/src/app/beans/bean-popover-list/bean-popover-list.component.ts new file mode 100644 index 000000000..7d565d153 --- /dev/null +++ b/src/app/beans/bean-popover-list/bean-popover-list.component.ts @@ -0,0 +1,68 @@ +import { + Component, + ElementRef, + HostListener, + Input, + OnInit, + ViewChild, +} from '@angular/core'; +import { Bean } from '../../../classes/bean/bean'; +import { AgVirtualSrollComponent } from 'ag-virtual-scroll'; +import { ModalController } from '@ionic/angular'; + +@Component({ + selector: 'app-bean-popover-list', + templateUrl: './bean-popover-list.component.html', + styleUrls: ['./bean-popover-list.component.scss'], +}) +export class BeanPopoverListComponent { + public static readonly COMPONENT_ID = 'bean-popover-list'; + + @Input() public beansList: Array = undefined; + + @ViewChild('openScroll', { read: AgVirtualSrollComponent, static: false }) + public openScroll: AgVirtualSrollComponent; + + @ViewChild('beanContent', { read: ElementRef }) + public beanContent: ElementRef; + + constructor(private readonly modalController: ModalController) {} + + public async ionViewWillEnter() { + this.loadBrews(); + } + + @HostListener('window:resize') + @HostListener('window:orientationchange', ['$event']) + public onOrientationChange(_event: any) { + this.retriggerScroll(); + } + + private retriggerScroll() { + setTimeout(async () => { + const el = this.beanContent.nativeElement; + let scrollComponent: AgVirtualSrollComponent; + scrollComponent = this.openScroll; + scrollComponent.el.style.height = + el.offsetHeight - scrollComponent.el.offsetTop + 'px'; + }, 150); + } + + public loadBrews() { + this.retriggerScroll(); + } + + public async brewAction(): Promise { + this.loadBrews(); + } + + public dismiss(): void { + this.modalController.dismiss( + { + dismissed: true, + }, + undefined, + BeanPopoverListComponent.COMPONENT_ID + ); + } +} diff --git a/src/app/beans/bean-sort/bean-sort.component.html b/src/app/beans/bean-sort/bean-sort.component.html index a5db09b4f..d7c7ed4d5 100644 --- a/src/app/beans/bean-sort/bean-sort.component.html +++ b/src/app/beans/bean-sort/bean-sort.component.html @@ -43,6 +43,10 @@ {{"BREW_DATA_RATING" | translate}} + + + {{"BEAN_SORT_BEAN_AGE" | translate}} + diff --git a/src/app/beans/beans-add/beans-add.component.ts b/src/app/beans/beans-add/beans-add.component.ts index 830946364..81f5afaf6 100644 --- a/src/app/beans/beans-add/beans-add.component.ts +++ b/src/app/beans/beans-add/beans-add.component.ts @@ -64,6 +64,69 @@ export class BeansAddComponent implements OnInit { } } + private async checkIfInformationAreSetButNotDisplayed() { + try { + const params = this.settings.bean_manage_parameters; + if (this.data.bean_information.length > 0) { + const info: IBeanInformation = this.data.bean_information[0]; + let hasDataSet: boolean = false; + if (info.country && info.country !== '') { + hasDataSet = true; + } + if (info.region && info.region !== '') { + hasDataSet = true; + } + if (info.farm && info.farm !== '') { + hasDataSet = true; + } + if (info.farmer && info.farmer !== '') { + hasDataSet = true; + } + if (info.elevation && info.elevation !== '') { + hasDataSet = true; + } + if (info.harvest_time && info.harvest_time !== '') { + hasDataSet = true; + } + if (info.variety && info.variety !== '') { + hasDataSet = true; + } + if (info.processing && info.processing !== '') { + hasDataSet = true; + } + if (info.certification && info.certification !== '') { + hasDataSet = true; + } + if (info.percentage && info.percentage > 0) { + hasDataSet = true; + } + if (info.purchasing_price && info.purchasing_price > 0) { + hasDataSet = true; + } + if (info.fob_price && info.fob_price > 0) { + hasDataSet = true; + } + + if (params.bean_information === false && hasDataSet === true) { + //Woopsi doopsi, user hasn't enabled the bean_information, lets display him a popover + //#623 + try { + const yes = await this.uiAlert.showConfirm( + 'BEAN_POPUP_YOU_DONT_SEE_EVERYTHING_DESCRIPTION', + 'INFORMATION', + true + ); + this.settings.bean_manage_parameters.bean_information = true; + await this.uiSettingsStorage.update(this.settings); + //Activate + } catch (ex) { + // Don't activate + } + } + } + } catch (ex) {} + } + public async ionViewWillEnter() { this.uiAnalytics.trackEvent(BEAN_TRACKING.TITLE, BEAN_TRACKING.ACTIONS.ADD); @@ -71,6 +134,7 @@ export class BeansAddComponent implements OnInit { // TODO how to handle roasting beans which wil be repeated? if (this.bean_template) { await this.__loadBean(this.bean_template); + await this.checkIfInformationAreSetButNotDisplayed(); } // Download images after loading the bean, else they would be copied :O diff --git a/src/app/beans/beans.page.html b/src/app/beans/beans.page.html index 73bb7d49a..97fb274ce 100644 --- a/src/app/beans/beans.page.html +++ b/src/app/beans/beans.page.html @@ -74,6 +74,10 @@ + + + +
@@ -85,8 +89,8 @@
- - + @@ -109,6 +113,10 @@ + + + +
@@ -118,8 +126,8 @@ {{"PAGE_BEANS_LIST_YOU_GOT_NO_FROZEN_BEANS" | translate}}
- - + @@ -142,6 +150,10 @@ + + + +
@@ -151,8 +163,8 @@ {{"PAGE_BEANS_LIST_YOU_GOT_NO_FINISHED_BEANS" | translate}}
- - + diff --git a/src/app/beans/beans.page.ts b/src/app/beans/beans.page.ts index c47a41e5d..3e70c8499 100644 --- a/src/app/beans/beans.page.ts +++ b/src/app/beans/beans.page.ts @@ -74,6 +74,10 @@ export class BeansPage implements OnDestroy { sort_order: BEAN_SORT_ORDER.UNKOWN, }; + public openBeansCollapsed: boolean = false; + public archivedBeansCollapsed: boolean = false; + public frozenBeansCollapsed: boolean = false; + public archivedBeansFilterText: string = ''; public openBeansFilterText: string = ''; public frozenBeansFilterText: string = ''; @@ -100,10 +104,15 @@ export class BeansPage implements OnDestroy { this.settings = this.uiSettingsStorage.getSettings(); this.archivedBeansSort = this.settings.bean_sort.ARCHIVED; this.openBeansSort = this.settings.bean_sort.OPEN; + this.frozenBeansSort = this.settings.bean_sort.FROZEN; this.archivedBeansFilter = this.settings.bean_filter.ARCHIVED; this.frozenBeansFilter = this.settings.bean_filter.FROZEN; this.openBeansFilter = this.settings.bean_filter.OPEN; + + this.openBeansCollapsed = this.settings.bean_collapsed.OPEN; + this.archivedBeansCollapsed = this.settings.bean_collapsed.ARCHIVED; + this.frozenBeansCollapsed = this.settings.bean_collapsed.FROZEN; this.loadBeans(); this.beanStorageChangeSubscription = this.uiBeanStorage @@ -119,6 +128,36 @@ export class BeansPage implements OnDestroy { this.beanStorageChangeSubscription = undefined; } } + public isCollapseActive() { + let collapsed: boolean = false; + if (this.bean_segment === 'open') { + collapsed = this.openBeansCollapsed; + } else if (this.bean_segment === 'archive') { + collapsed = this.archivedBeansCollapsed; + } else if (this.bean_segment === 'frozen') { + collapsed = this.frozenBeansCollapsed; + } + return collapsed; + } + + public toggleCollapseBeans() { + if (this.bean_segment === 'open') { + this.openBeansCollapsed = !this.openBeansCollapsed; + } else if (this.bean_segment === 'archive') { + this.archivedBeansCollapsed = !this.archivedBeansCollapsed; + } else if (this.bean_segment === 'frozen') { + this.frozenBeansCollapsed = !this.frozenBeansCollapsed; + } + this.__saveCollapseFilter(); + this.research(); + } + private async __saveCollapseFilter() { + this.settings.bean_collapsed.OPEN = this.openBeansCollapsed; + this.settings.bean_collapsed.ARCHIVED = this.archivedBeansCollapsed; + this.settings.bean_collapsed.FROZEN = this.frozenBeansCollapsed; + await this.uiSettingsStorage.saveSettings(this.settings); + } + public loadBeans(): void { this.__initializeBeans(); this.changeDetectorRef.detectChanges(); @@ -495,10 +534,10 @@ export class BeansPage implements OnDestroy { break; case BEAN_SORT_AFTER.ROASTING_DATE: filterBeans = filterBeans.sort((a, b) => { - if (a.roastingDate > b.roastingDate) { + if (a.roastingDate < b.roastingDate) { return -1; } - if (a.roastingDate < b.roastingDate) { + if (a.roastingDate > b.roastingDate) { return 1; } return 0; @@ -506,10 +545,21 @@ export class BeansPage implements OnDestroy { break; case BEAN_SORT_AFTER.RATING: filterBeans = filterBeans.sort((a, b) => { + if (a.rating < b.rating) { + return -1; + } if (a.rating > b.rating) { + return 1; + } + return 0; + }); + break; + case BEAN_SORT_AFTER.BEAN_AGE: + filterBeans = filterBeans.sort((a, b) => { + if (a.beanAgeInDays() < b.beanAgeInDays()) { return -1; } - if (a.rating < b.rating) { + if (a.beanAgeInDays() > b.beanAgeInDays()) { return 1; } return 0; diff --git a/src/app/brew/brew-add/brew-add.component.html b/src/app/brew/brew-add/brew-add.component.html index 879e5da1a..1ebe900a8 100644 --- a/src/app/brew/brew-add/brew-add.component.html +++ b/src/app/brew/brew-add/brew-add.component.html @@ -18,6 +18,16 @@ + + + + + + + + + + diff --git a/src/app/brew/brew-choose-preparation-to-brew/brew-choose-preparation-to-brew.component.html b/src/app/brew/brew-choose-preparation-to-brew/brew-choose-preparation-to-brew.component.html index f77150771..b2f769298 100644 --- a/src/app/brew/brew-choose-preparation-to-brew/brew-choose-preparation-to-brew.component.html +++ b/src/app/brew/brew-choose-preparation-to-brew/brew-choose-preparation-to-brew.component.html @@ -8,7 +8,8 @@ - {{prep.name}} ({{"LAST_USE" | translate}}: + {{prep.name}} ({{"LAST_USE" | translate}}: {{lastUsed(prep) | formatDate:[settings?.date_format]}} -) diff --git a/src/app/brew/brew-edit/brew-edit.component.html b/src/app/brew/brew-edit/brew-edit.component.html index 72bb081fe..3531024c4 100644 --- a/src/app/brew/brew-edit/brew-edit.component.html +++ b/src/app/brew/brew-edit/brew-edit.component.html @@ -11,10 +11,20 @@ - + + + + + + + + + + + diff --git a/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.html b/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.html new file mode 100644 index 000000000..9025f3160 --- /dev/null +++ b/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.html @@ -0,0 +1,42 @@ + + + {{"IMPORT_SHOT_FROM_METICULOUS" | translate}} + + + + + + + + + + + + + + + +

{{entry.profile.name}}

+
{{this.uiHelper.formatTimeNumber(entry.time * 1000)}}
+
+ +
+
+
+
+ + +
+
+ + + + {{"CANCEL" | translate}} + + + {{"CHOOSE" | translate}} + + + diff --git a/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.scss b/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.scss new file mode 100644 index 000000000..cbdca4145 --- /dev/null +++ b/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.scss @@ -0,0 +1,11 @@ +:host { + ion-item { + margin-left: 16px; + margin-right: 0px; + line-height: 12px; + font-size: 12px; + } + ion-card-content { + background:#FFFFFF; + } +} diff --git a/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.spec.ts b/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.spec.ts new file mode 100644 index 000000000..629913fa2 --- /dev/null +++ b/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { BrewModalImportShotMeticulousComponent } from './brew-modal-import-shot-meticulous.component'; +import { InAppBrowser } from '@awesome-cordova-plugins/in-app-browser/ngx'; +import { File } from '@awesome-cordova-plugins/file/ngx'; +import { SocialSharing } from '@awesome-cordova-plugins/social-sharing/ngx'; +import { FileTransfer } from '@awesome-cordova-plugins/file-transfer/ngx'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { TranslateServiceMock } from '../../../classes/mock'; +describe('BrewModalImportShotMeticulousComponent', () => { + let component: BrewModalImportShotMeticulousComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [BrewModalImportShotMeticulousComponent], + imports: [IonicModule.forRoot(), TranslateModule.forRoot()], + providers: [ + { provide: InAppBrowser }, + { provide: File }, + { provide: SocialSharing }, + { provide: FileTransfer }, + { + provide: TranslateService, + useValue: TranslateServiceMock, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(BrewModalImportShotMeticulousComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.ts b/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.ts new file mode 100644 index 000000000..3ffbddffa --- /dev/null +++ b/src/app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component.ts @@ -0,0 +1,107 @@ +import { + Component, + ElementRef, + HostListener, + Input, + OnInit, + ViewChild, +} from '@angular/core'; + +import { MeticulousDevice } from '../../../classes/preparationDevice/meticulous/meticulousDevice'; +import { ModalController } from '@ionic/angular'; +import { HistoryListingEntry } from '@meticulous-home/espresso-api/dist/types'; +import { UIHelper } from '../../../services/uiHelper'; +import { AgVirtualSrollComponent } from 'ag-virtual-scroll'; + +@Component({ + selector: 'app-brew-modal-import-shot-meticulous', + templateUrl: './brew-modal-import-shot-meticulous.component.html', + styleUrls: ['./brew-modal-import-shot-meticulous.component.scss'], +}) +export class BrewModalImportShotMeticulousComponent implements OnInit { + public static COMPONENT_ID: string = 'brew-modal-import-shot-meticulous'; + + @Input() public meticulousDevice: MeticulousDevice; + public radioSelection: string; + public history: Array = []; + + @ViewChild('ionItemEl', { read: ElementRef, static: false }) + public ionItemEl: ElementRef; + + @ViewChild('historyShotContent', { read: ElementRef }) + public historyShotContent: ElementRef; + + @ViewChild('shotDataScroll', { + read: AgVirtualSrollComponent, + static: false, + }) + public shotDataScroll: AgVirtualSrollComponent; + + @ViewChild('footerContent', { read: ElementRef }) + public footerContent: ElementRef; + + constructor( + private readonly modalController: ModalController, + public readonly uiHelper: UIHelper + ) {} + + public ngOnInit() { + this.readHistory(); + } + + private async readHistory() { + this.history = await this.meticulousDevice?.getHistory(); + this.retriggerScroll(); + } + @HostListener('window:resize') + @HostListener('window:orientationchange', ['$event']) + public onOrientationChange(event) { + this.retriggerScroll(); + } + + private retriggerScroll() { + setTimeout(async () => { + const el = this.historyShotContent.nativeElement; + const scrollComponent: AgVirtualSrollComponent = this.shotDataScroll; + + if (scrollComponent) { + scrollComponent.el.style.height = el.offsetHeight - 20 + 'px'; + } + }, 150); + } + + public getElementOffsetWidth() { + if (this.ionItemEl?.nativeElement?.offsetWidth) { + return this.ionItemEl?.nativeElement?.offsetWidth - 50; + } + return 0; + } + + public dismiss(): void { + this.modalController.dismiss( + { + dismissed: true, + }, + undefined, + BrewModalImportShotMeticulousComponent.COMPONENT_ID + ); + } + public choose(): void { + let returningData; + + for (const entry of this.history) { + if (entry.id === this.radioSelection) { + returningData = entry; + break; + } + } + this.modalController.dismiss( + { + choosenHistory: returningData, + dismissed: true, + }, + undefined, + BrewModalImportShotMeticulousComponent.COMPONENT_ID + ); + } +} diff --git a/src/app/brew/brew-popover-actions/brew-popover-actions.component.html b/src/app/brew/brew-popover-actions/brew-popover-actions.component.html index 02819e3e7..79e2f04b4 100644 --- a/src/app/brew/brew-popover-actions/brew-popover-actions.component.html +++ b/src/app/brew/brew-popover-actions/brew-popover-actions.component.html @@ -55,6 +55,10 @@ {{"SHOW_VISUALIZER" | translate}} + + + + {{"SHOW_GRAPH" | translate}} diff --git a/src/app/brew/brew.page.html b/src/app/brew/brew.page.html index 6bdb6ffd4..776ca7fdb 100644 --- a/src/app/brew/brew.page.html +++ b/src/app/brew/brew.page.html @@ -55,6 +55,12 @@ + + + + + +
@@ -65,8 +71,8 @@
- - + @@ -84,6 +90,10 @@ + + + +
@@ -94,8 +104,8 @@
- - + diff --git a/src/app/brew/brew.page.ts b/src/app/brew/brew.page.ts index dfb6e6188..73b55b437 100644 --- a/src/app/brew/brew.page.ts +++ b/src/app/brew/brew.page.ts @@ -6,8 +6,7 @@ import { OnInit, ViewChild, } from '@angular/core'; -import { ModalController, Platform } from '@ionic/angular'; -import { UIAlert } from '../../services/uiAlert'; +import { ModalController } from '@ionic/angular'; import { UIHelper } from '../../services/uiHelper'; import { UIBrewStorage } from '../../services/uiBrewStorage'; import { UISettingsStorage } from '../../services/uiSettingsStorage'; @@ -19,7 +18,6 @@ import { Bean } from '../../classes/bean/bean'; import { BrewFilterComponent } from './brew-filter/brew-filter.component'; import { Settings } from '../../classes/settings/settings'; import { AgVirtualSrollComponent } from 'ag-virtual-scroll'; -import { UIAnalytics } from '../../services/uiAnalytics'; @Component({ selector: 'brew', @@ -49,18 +47,18 @@ export class BrewPage implements OnInit { public archivedBrewsFilter: IBrewPageFilter; public openBrewsFilter: IBrewPageFilter; + public openBrewsCollapsed: boolean = false; + public archivedBrewsCollapsed: boolean = false; + public settings: Settings; constructor( private readonly modalCtrl: ModalController, - private readonly platform: Platform, private readonly uiBrewStorage: UIBrewStorage, private readonly changeDetectorRef: ChangeDetectorRef, - private readonly uiAlert: UIAlert, public uiHelper: UIHelper, public uiBrewHelper: UIBrewHelper, - private readonly uiSettingsStorage: UISettingsStorage, - private readonly uiAnalytics: UIAnalytics + private readonly uiSettingsStorage: UISettingsStorage ) { this.settings = this.uiSettingsStorage.getSettings(); this.archivedBrewsFilter = this.settings.GET_BREW_FILTER(); @@ -70,6 +68,8 @@ export class BrewPage implements OnInit { public ionViewWillEnter(): void { this.archivedBrewsFilter = this.settings.brew_filter.ARCHIVED; this.openBrewsFilter = this.settings.brew_filter.OPEN; + this.openBrewsCollapsed = this.settings.brew_collapsed.OPEN; + this.archivedBrewsCollapsed = this.settings.brew_collapsed.ARCHIVED; this.loadBrews(); this.retriggerScroll(); @@ -123,6 +123,26 @@ export class BrewPage implements OnInit { this.__initializeBrewView('archiv'); } + public isCollapseActive() { + let collapsed: boolean = false; + if (this.brew_segment === 'open') { + collapsed = this.openBrewsCollapsed; + } else { + collapsed = this.archivedBrewsCollapsed; + } + return collapsed; + } + + public toggleCollapseBrews() { + if (this.brew_segment === 'open') { + this.openBrewsCollapsed = !this.openBrewsCollapsed; + } else { + this.archivedBrewsCollapsed = !this.archivedBrewsCollapsed; + } + this.__saveCollapseFilter(); + this.research(); + } + public isFilterActive(): boolean { let checkingFilter: IBrewPageFilter; let checkingFilterText: string = ''; @@ -237,6 +257,11 @@ export class BrewPage implements OnInit { this.settings.brew_filter.ARCHIVED = this.archivedBrewsFilter; await this.uiSettingsStorage.saveSettings(this.settings); } + private async __saveCollapseFilter() { + this.settings.brew_collapsed.OPEN = this.openBrewsCollapsed; + this.settings.brew_collapsed.ARCHIVED = this.archivedBrewsCollapsed; + await this.uiSettingsStorage.saveSettings(this.settings); + } public research() { this.__initializeBrewView(this.brew_segment); diff --git a/src/app/graph-section/graph/graph-detail/graph-detail.component.ts b/src/app/graph-section/graph/graph-detail/graph-detail.component.ts index 4bc1b9dd5..8999fb49f 100644 --- a/src/app/graph-section/graph/graph-detail/graph-detail.component.ts +++ b/src/app/graph-section/graph/graph-detail/graph-detail.component.ts @@ -11,7 +11,6 @@ import BeanconquerorFlowTestDataDummy from '../../../../assets/BeanconquerorFlow import { BrewFlow } from '../../../../classes/brew/brewFlow'; import { Settings } from '../../../../classes/settings/settings'; import { TranslateService } from '@ngx-translate/core'; -import { Graph } from '../../../../classes/graph/graph'; import { IGraph } from '../../../../interfaces/graph/iGraph'; import GRAPH_TRACKING from '../../../../data/tracking/graphTracking'; import { UIAnalytics } from '../../../../services/uiAnalytics'; @@ -19,6 +18,7 @@ import { UIHelper } from '../../../../services/uiHelper'; import { ModalController, Platform } from '@ionic/angular'; import { UIFileHelper } from '../../../../services/uiFileHelper'; import { UISettingsStorage } from '../../../../services/uiSettingsStorage'; +import { IBrew } from '../../../../interfaces/brew/iBrew'; declare var Plotly; @Component({ selector: 'app-graph-detail', @@ -44,9 +44,9 @@ export class GraphDetailComponent implements OnInit { @ViewChild('profileDiv', { read: ElementRef, static: true }) public profileDiv: ElementRef; - public data: Graph = new Graph(); - @Input() private graph: IGraph; + @Input() private brew: IBrew; + @Input() private flowProfileData: any; constructor( @@ -68,8 +68,9 @@ export class GraphDetailComponent implements OnInit { GRAPH_TRACKING.ACTIONS.DETAIL ); if (this.graph) { - this.data = this.uiHelper.copyData(this.graph); - await this.readFlowProfile(); + await this.readFlowProfile(this.graph.flow_profile); + } else if (this.brew) { + await this.readFlowProfile(this.brew.flow_profile); } else { this.flow_profile_raw = this.uiHelper.cloneData(this.flowProfileData); } @@ -174,6 +175,16 @@ export class GraphDetailComponent implements OnInit { }, }; + const graph_pressure_settings = this.settings.graph_pressure; + const suggestedMinPressure: number = graph_pressure_settings.lower; + let suggestedMaxPressure = graph_pressure_settings.upper; + try { + if (this.pressureTrace?.y.length > 0) { + suggestedMaxPressure = Math.max(...this.pressureTrace.y); + suggestedMaxPressure = Math.ceil(suggestedMaxPressure + 1); + } + } catch (ex) {} + layout['yaxis4'] = { title: '', titlefont: { color: '#05C793' }, @@ -183,7 +194,7 @@ export class GraphDetailComponent implements OnInit { side: 'right', showgrid: false, position: 0.93, - range: [0, 10], + range: [suggestedMinPressure, suggestedMaxPressure], visible: true, }; @@ -349,6 +360,7 @@ export class GraphDetailComponent implements OnInit { } } } + if ( this.flow_profile_raw?.pressureFlow && this.flow_profile_raw.pressureFlow.length > 0 @@ -397,13 +409,11 @@ export class GraphDetailComponent implements OnInit { }, 100); } - private async readFlowProfile() { + private async readFlowProfile(_path) { if (this.platform.is('cordova')) { - if (this.graph.flow_profile !== '') { + if (_path !== '') { try { - const jsonParsed = await this.uiFileHelper.getJSONFile( - this.graph.flow_profile - ); + const jsonParsed = await this.uiFileHelper.getJSONFile(_path); this.flow_profile_raw = jsonParsed; } catch (ex) {} } diff --git a/src/app/preparation/preparation-add-type/preparation-add-type.component.ts b/src/app/preparation/preparation-add-type/preparation-add-type.component.ts index 4d2a4a228..09385ab7b 100644 --- a/src/app/preparation/preparation-add-type/preparation-add-type.component.ts +++ b/src/app/preparation/preparation-add-type/preparation-add-type.component.ts @@ -11,6 +11,7 @@ import { PREPARATION_STYLE_TYPE } from '../../../enums/preparations/preparationS import { PreparationTool } from '../../../classes/preparation/preparationTool'; import PREPARATION_TRACKING from '../../../data/tracking/preparationTracking'; import { UIAnalytics } from '../../../services/uiAnalytics'; +import { UIPreparationHelper } from '../../../services/uiPreparationHelper'; @Component({ selector: 'preparation-add-type', @@ -34,9 +35,11 @@ export class PreparationAddTypeComponent implements OnInit { private readonly navParams: NavParams, private readonly uiToast: UIToast, private readonly translate: TranslateService, - private readonly uiAnalytics: UIAnalytics + private readonly uiAnalytics: UIAnalytics, + private readonly uiPreparationHelper: UIPreparationHelper ) { this.data.type = this.navParams.get('type'); + if (this.data.type !== PREPARATION_TYPES.CUSTOM_PREPARATION) { this.data.name = this.translate.instant( 'PREPARATION_TYPE_' + this.data.type @@ -69,11 +72,19 @@ export class PreparationAddTypeComponent implements OnInit { this.data.manage_parameters.coffee_first_drip_time = false; this.data.default_last_coffee_parameters.coffee_first_drip_time = false; } - await this.uiPreparationStorage.add(this.data); + const newPreparation = await this.uiPreparationStorage.add(this.data); this.dismiss(true); if (!this.hide_toast_message) { this.uiToast.showInfoToast('TOAST_PREPARATION_ADDED_SUCCESSFULLY'); } + + if ( + this.data.type === PREPARATION_TYPES.METICULOUS || + this.data.type === PREPARATION_TYPES.XENIA || + this.data.type === PREPARATION_TYPES.SANREMO_YOU + ) { + await this.uiPreparationHelper.connectDevice(newPreparation); + } } public async dismiss(_added: boolean) { diff --git a/src/app/preparation/preparation-add/preparation-add.component.html b/src/app/preparation/preparation-add/preparation-add.component.html index 590de67ee..7a6d4394b 100644 --- a/src/app/preparation/preparation-add/preparation-add.component.html +++ b/src/app/preparation/preparation-add/preparation-add.component.html @@ -12,28 +12,31 @@ - - - + -
- -
+ + + + +
+ +
-
- {{'PREPARATION_TYPE_' + key | translate}} -
-
-
- + {{'PREPARATION_TYPE_' + key | translate}} +
+
+ +
+ +
+
+ {{'PREPARATION_TYPE_ADD_CUSTOM' | translate}}
-
- {{'PREPARATION_TYPE_ADD_CUSTOM' | translate}} -
-
-
-
+
+
+
+
diff --git a/src/app/preparation/preparation-add/preparation-add.component.ts b/src/app/preparation/preparation-add/preparation-add.component.ts index 47f688498..cd800c9ef 100644 --- a/src/app/preparation/preparation-add/preparation-add.component.ts +++ b/src/app/preparation/preparation-add/preparation-add.component.ts @@ -7,6 +7,7 @@ import { NgForm } from '@angular/forms'; import { PreparationAddTypeComponent } from '../preparation-add-type/preparation-add-type.component'; import PREPARATION_TRACKING from '../../../data/tracking/preparationTracking'; import { UIAnalytics } from '../../../services/uiAnalytics'; +import { environment } from '../../../environments/environment'; @Component({ selector: 'preparation-add', templateUrl: './preparation-add.component.html', @@ -18,6 +19,8 @@ export class PreparationAddComponent implements OnInit { public preparation_types_enum = PREPARATION_TYPES; + public ENVIRONMENT = environment; + @ViewChild('addPreparationForm', { static: false }) public preparationForm: NgForm; @@ -35,6 +38,16 @@ export class PreparationAddComponent implements OnInit { ); } + public individualPreparationVisible(_key) { + if (_key === 'SANREMO_YOU') { + if (this.ENVIRONMENT.FEATURES_ACTIVE.SANREMO_YOU === true) { + return true; + } + return false; + } + return true; + } + public async choosePreparation(_prepType: PREPARATION_TYPES) { //Animated false, else backdrop would sometimes not disappear and stay until user touches again. const modal = await this.modalController.create({ diff --git a/src/app/preparation/preparation-connected-device/preparation-connected-device.component.html b/src/app/preparation/preparation-connected-device/preparation-connected-device.component.html index 42bf70628..85fca6bce 100644 --- a/src/app/preparation/preparation-connected-device/preparation-connected-device.component.html +++ b/src/app/preparation/preparation-connected-device/preparation-connected-device.component.html @@ -12,18 +12,19 @@ - {{"PREPARATION_DEVICE.TYPE.NONE" | translate}} {{"PREPARATION_DEVICE.TYPE.XENIA" | translate}} {{"PREPARATION_DEVICE.TYPE.METICULOUS" | translate}} + {{"PREPARATION_DEVICE.TYPE.SANREMO_YOU" | translate}} - + {{"PREPARATION_DEVICE.URL" | translate}} - @@ -31,7 +32,7 @@ - V1 V2 @@ -40,7 +41,7 @@

{{"PREPARATION_DEVICE.RESIDUAL_LAG_TIME" | translate }} {{uiHelper.toFixedIfNecessary(preparation.connectedPreparationDevice.customParams.residualLagTime,2)}}

+ style="vertical-align: top;">{{uiHelper.toFixedIfNecessary(data.connectedPreparationDevice.customParams.residualLagTime,2)}}
@@ -49,33 +50,59 @@

{{"PREPARATION_DEVICE.RESIDUAL_LAG_TIME" | translate }}  - - +
{{"SAVE_LOGFILES_TO_NOTES_FROM_MACHINE" | translate}}
- - + + + {{"PREPARATION_DEVICE.URL" | translate}} + + + + + + + + {{"PREPARATION_DEVICE.URL" | translate}} - + + + + + +

{{"PREPARATION_DEVICE.RESIDUAL_LAG_TIME" | translate }} {{uiHelper.toFixedIfNecessary(data.connectedPreparationDevice.customParams.residualLagTime,2)}}

+
+
+ + +

{{"PREPARATION_DEVICE.RESIDUAL_LAG_TIME_DESCRIPTION" | translate }}

+
+
+ + + + +
- - - - + diff --git a/src/app/preparation/preparation-connected-device/preparation-connected-device.component.ts b/src/app/preparation/preparation-connected-device/preparation-connected-device.component.ts index e4425e92c..a4b55d94c 100644 --- a/src/app/preparation/preparation-connected-device/preparation-connected-device.component.ts +++ b/src/app/preparation/preparation-connected-device/preparation-connected-device.component.ts @@ -12,6 +12,11 @@ import { UIAlert } from '../../../services/uiAlert'; import { UIHelper } from '../../../services/uiHelper'; import { UISettingsStorage } from '../../../services/uiSettingsStorage'; import { Settings } from '../../../classes/settings/settings'; +import { environment } from '../../../environments/environment'; +import { PREPARATION_TYPES } from '../../../enums/preparations/preparationTypes'; +import { SanremoYOUParams } from '../../../classes/preparationDevice/sanremo/sanremoYOUDevice'; +import { MeticulousParams } from '../../../classes/preparationDevice/meticulous/meticulousDevice'; +import { XeniaParams } from '../../../classes/preparationDevice/xenia/xeniaDevice'; @Component({ selector: 'app-preparation-connected-device', @@ -26,6 +31,8 @@ export class PreparationConnectedDeviceComponent { public PREPARATION_DEVICE_TYPE = PreparationDeviceType; @Input() public preparation: IPreparation; + public ENVIRONMENT_PARAMS = environment; + public pinFormatter(value: any) { const parsedFloat = parseFloat(value); if (isNaN(parsedFloat)) { @@ -48,6 +55,26 @@ export class PreparationConnectedDeviceComponent { if (this.preparation !== undefined) { this.data.initializeByObject(this.preparation); } + if ( + this.data.connectedPreparationDevice.type === PreparationDeviceType.NONE + ) { + if (this.data.type === PREPARATION_TYPES.METICULOUS) { + this.data.connectedPreparationDevice.type = + PreparationDeviceType.METICULOUS; + this.data.connectedPreparationDevice.customParams = + new MeticulousParams(); + } + if (this.data.type === PREPARATION_TYPES.XENIA) { + this.data.connectedPreparationDevice.type = PreparationDeviceType.XENIA; + this.data.connectedPreparationDevice.customParams = new XeniaParams(); + } + if (this.data.type === PREPARATION_TYPES.SANREMO_YOU) { + this.data.connectedPreparationDevice.type = + PreparationDeviceType.SANREMO_YOU; + this.data.connectedPreparationDevice.customParams = + new SanremoYOUParams(); + } + } } public dismiss(): void { @@ -98,7 +125,36 @@ export class PreparationConnectedDeviceComponent { this.data.connectedPreparationDevice.customParams.residualLagTime = 1.35; } } - + if ( + this.data.connectedPreparationDevice.type === + PreparationDeviceType.METICULOUS + ) { + if (this.data.connectedPreparationDevice.url.endsWith('/') === true) { + this.data.connectedPreparationDevice.url = + this.data.connectedPreparationDevice.url.slice(0, -1); + } + if ( + this.data.connectedPreparationDevice.url.startsWith('http') === false + ) { + this.data.connectedPreparationDevice.url = + 'http://' + this.data.connectedPreparationDevice.url; + } + } + if ( + this.data.connectedPreparationDevice.type === + PreparationDeviceType.SANREMO_YOU + ) { + if (this.data.connectedPreparationDevice.url.endsWith('/') === true) { + this.data.connectedPreparationDevice.url = + this.data.connectedPreparationDevice.url.slice(0, -1); + } + if ( + this.data.connectedPreparationDevice.url.startsWith('http') === false + ) { + this.data.connectedPreparationDevice.url = + 'http://' + this.data.connectedPreparationDevice.url; + } + } if ( this.data.connectedPreparationDevice.type !== PreparationDeviceType.NONE ) { diff --git a/src/app/preparation/preparation-modal-select/preparation-modal-select.component.html b/src/app/preparation/preparation-modal-select/preparation-modal-select.component.html index 5d775696c..3151de2b7 100644 --- a/src/app/preparation/preparation-modal-select/preparation-modal-select.component.html +++ b/src/app/preparation/preparation-modal-select/preparation-modal-select.component.html @@ -28,7 +28,8 @@ -
{{prep.name}}
+
{{prep.name}}

{{"LAST_USE" | translate}}: {{lastUsed(prep) | formatDate:[settings?.date_format]}} - @@ -47,7 +48,8 @@ -

{{prep.name}}
+
{{prep.name}}

{{"LAST_USE" | translate}}: {{lastUsed(prep) | formatDate:[settings?.date_format]}} - @@ -72,7 +74,8 @@

{{prep.name}}
-
{{prep.name}}
+
{{prep.name}}

{{"LAST_USE" | translate}}: {{lastUsed(prep) | formatDate:[settings?.date_format]}} - @@ -91,7 +94,8 @@

{{prep.name}}
-
{{prep.name}}
+
{{prep.name}}

{{"LAST_USE" | translate}}: {{lastUsed(prep) | formatDate:[settings?.date_format]}} - diff --git a/src/app/preparation/preparation-sort-tools/preparation-sort-tools.component.ts b/src/app/preparation/preparation-sort-tools/preparation-sort-tools.component.ts index ad1c717fe..d0eac0ffb 100644 --- a/src/app/preparation/preparation-sort-tools/preparation-sort-tools.component.ts +++ b/src/app/preparation/preparation-sort-tools/preparation-sort-tools.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { Preparation } from '../../../classes/preparation/preparation'; import { ModalController } from '@ionic/angular'; +import { UIHelper } from '../../../services/uiHelper'; @Component({ selector: 'app-preparation-sort-tools', @@ -20,7 +21,8 @@ export class PreparationSortToolsComponent implements OnInit { @Input() public preparation: Preparation; constructor( private readonly modalController: ModalController, - private readonly changeDetectorRef: ChangeDetectorRef + private readonly changeDetectorRef: ChangeDetectorRef, + private readonly uiHelper: UIHelper ) {} public ngOnInit() { @@ -40,13 +42,12 @@ export class PreparationSortToolsComponent implements OnInit { reorderVar.splice(ev.detail.to, 0, reorderVar.splice(ev.detail.from, 1)[0]); - const swapElements = (myArray, index1, index2) => { - [myArray[index1], myArray[index2]] = [myArray[index2], myArray[index1]]; - }; - - swapElements(this.preparation.tools, ev.detail.from, ev.detail.to); + this.preparation.tools.splice( + ev.detail.to, + 0, + this.preparation.tools.splice(ev.detail.from, 1)[0] + ); - // console.log(this.settings.brew_order); // Finish the reorder and position the item in the DOM based on // where the gesture ended. This method can also be called directly // by the reorder group diff --git a/src/app/settings/settings.page.html b/src/app/settings/settings.page.html index 33306782b..cd21b706a 100644 --- a/src/app/settings/settings.page.html +++ b/src/app/settings/settings.page.html @@ -55,12 +55,20 @@

{{"EXPORT" | translate}}

Temp: Export Brew By Weight - Xenia
- + {{"EXCEL_EXPORT" | translate}} - - {{"IMPORT_BEANS_EXCEL" | translate}} + + {{"IMPORT_ROASTED_BEANS_EXCEL" | translate}} + + + + {{"IMPORT_GREEN_BEANS_EXCEL" | translate}} + + + + {{"DOWNLOAD_IMPORT_EXCEL_TEMPLATES" | translate}} @@ -84,6 +92,7 @@

{{"EXPORT" | translate}}

{{"PAGE_SETTINGS_LANGUAGE_FRANCE" | translate}} {{"PAGE_SETTINGS_LANGUAGE_INDONESIA" | translate}} {{"PAGE_SETTINGS_LANGUAGE_POLISH" | translate}} + {{"PAGE_SETTINGS_LANGUAGE_DUTCH" | translate}}
@@ -361,7 +370,7 @@

{{"PAGE_SETTINGS_BREW_TIMER_START_DELAY_ACTIVE" | translate}}

{{"PAGE_SETTINGS_MANAGE_FEATURES" | translate}} -
{{"ACTIVE_BEAN_FREEZING_FEATURE" | translate}}
@@ -982,6 +991,25 @@

{{"PRESSURE_LOG" | translate}}

+ + + + {{"PAGE_SETTINGS_TAB_BLUETOOTH_PRESSURE_GRAPHS_AXIS" | translate }} +

{{"PAGE_SETTINGS_TAB_BLUETOOTH_PRESSURE_GRAPHS_AXIS_DESCRIPTION" | translate}}

+
+ + + {{"PAGE_SETTINGS_TAB_BLUETOOTH_PRESSURE_GRAPHS_AXIS_RANGE" | translate}} {{this.uiHelper.toFixedIfNecessary(settings.graph_pressure.lower,2)}} - {{this.uiHelper.toFixedIfNecessary(settings.graph_pressure.upper,2)}} + + + + + + + +
+
diff --git a/src/app/settings/settings.page.ts b/src/app/settings/settings.page.ts index 46b8bd9b0..787af73aa 100644 --- a/src/app/settings/settings.page.ts +++ b/src/app/settings/settings.page.ts @@ -14,7 +14,6 @@ import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { DirectoryEntry, FileEntry } from '@awesome-cordova-plugins/file'; import { FileChooser } from '@awesome-cordova-plugins/file-chooser/ngx'; import { File } from '@awesome-cordova-plugins/file/ngx'; -import { FilePath } from '@awesome-cordova-plugins/file-path/ngx'; import { IBean } from '../../interfaces/bean/iBean'; import { IBrew } from '../../interfaces/brew/iBrew'; import { ISettings } from '../../interfaces/settings/iSettings'; @@ -918,6 +917,9 @@ export class SettingsPage { public async resetFilter() { this.settings.resetFilter(); } + public async resetBeanSort() { + this.settings.resetBeanSort(); + } public async fixWeightChangeMinFlowNumber() { //We need to trigger this, because the slider sometimes procudes values like 0.60000001, and we need to fix this before saving @@ -1208,6 +1210,15 @@ export class SettingsPage { this.uiExcel.export(); } + public pinFormatter(value: any) { + const parsedFloat = parseFloat(value); + if (isNaN(parsedFloat)) { + return `${0}`; + } + const newValue = +parsedFloat.toFixed(2); + return `${newValue}`; + } + public doWeHaveBrewByWeights(): boolean { const allPreparations = this.uiPreparationStorage.getAllEntries(); for (const prep of allPreparations) { @@ -1223,7 +1234,9 @@ export class SettingsPage { await this.uiAlert.showLoadingSpinner(); try { const allXeniaPreps = []; - const allPreparations = this.uiPreparationStorage.getAllEntries(); + let allPreparations = this.uiPreparationStorage.getAllEntries(); + // Just take 60, else the excel will be exploding. + allPreparations = allPreparations.reverse().slice(0, 60); for (const prep of allPreparations) { if ( prep.connectedPreparationDevice.type === PreparationDeviceType.XENIA @@ -1274,7 +1287,13 @@ export class SettingsPage { } } - public importBeansExcel(): void { + public downloadImportExcelTemplates() { + this.uiHelper.openExternalWebpage( + 'https://beanconqueror.gitbook.io/beanconqueror/resources/files' + ); + } + + public importBeansExcel(_type: string = 'roasted'): void { if (this.platform.is('cordova')) { this.uiAnalytics.trackEvent( SETTINGS_TRACKING.TITLE, @@ -1291,7 +1310,11 @@ export class SettingsPage { this.uiFileHelper.readFileEntryAsArrayBuffer(fileEntry).then( async (_arrayBuffer) => { - this.uiExcel.importBeansByExcel(_arrayBuffer); + if (_type === 'roasted') { + this.uiExcel.importBeansByExcel(_arrayBuffer); + } else { + this.uiExcel.importGreenBeansByExcel(_arrayBuffer); + } }, () => { // Backup, maybe it was a .JSON? @@ -1317,7 +1340,11 @@ export class SettingsPage { } this.uiFileHelper.readFileAsArrayBuffer(path, file).then( async (_arrayBuffer) => { - this.uiExcel.importBeansByExcel(_arrayBuffer); + if (_type === 'roasted') { + this.uiExcel.importBeansByExcel(_arrayBuffer); + } else { + this.uiExcel.importGreenBeansByExcel(_arrayBuffer); + } }, () => { // Backup, maybe it was a .JSON? diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index d81555727..731d7b0f2 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -189,6 +189,9 @@ import { BeanPopoverFreezeComponent } from '../beans/bean-popover-freeze/bean-po import { BeanFreezeInformationComponent } from '../../components/beans/bean-freeze-information/bean-freeze-information.component'; import { BeanPopoverFrozenListComponent } from '../beans/bean-popover-frozen-list/bean-popover-frozen-list.component'; import { PipesModule } from 'src/pipes/pipes.module'; +import { BrewModalImportShotMeticulousComponent } from '../brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component'; +import { DataCorruptionFoundComponent } from '../../popover/data-corruption-found/data-corruption-found.component'; +import { BeanPopoverListComponent } from '../beans/bean-popover-list/bean-popover-list.component'; @NgModule({ declarations: [ @@ -214,6 +217,7 @@ import { PipesModule } from 'src/pipes/pipes.module'; BeansAddComponent, BrewFlowComponent, BrewChooseGraphReferenceComponent, + BrewModalImportShotMeticulousComponent, BrewMaximizeControlsComponent, BeansEditComponent, BeansDetailComponent, @@ -230,6 +234,7 @@ import { PipesModule } from 'src/pipes/pipes.module'; WelcomePopoverComponent, AnalyticsPopoverComponent, MeticulousHelpPopoverComponent, + DataCorruptionFoundComponent, QrCodeScannerPopoverComponent, UpdatePopoverComponent, DatetimePopoverComponent, @@ -315,6 +320,7 @@ import { PipesModule } from 'src/pipes/pipes.module'; BeanPopoverAddComponent, BeanPopoverFreezeComponent, BeanPopoverFrozenListComponent, + BeanPopoverListComponent, BeanArchivePopoverComponent, MillPopoverActionsComponent, BeanModalSelectComponent, @@ -420,6 +426,7 @@ import { PipesModule } from 'src/pipes/pipes.module'; BeansAddComponent, BrewFlowComponent, BrewChooseGraphReferenceComponent, + BrewModalImportShotMeticulousComponent, BrewMaximizeControlsComponent, BeansEditComponent, BrewRatingComponent, @@ -441,6 +448,7 @@ import { PipesModule } from 'src/pipes/pipes.module'; WelcomePopoverComponent, AnalyticsPopoverComponent, MeticulousHelpPopoverComponent, + DataCorruptionFoundComponent, QrCodeScannerPopoverComponent, UpdatePopoverComponent, DatetimePopoverComponent, @@ -523,6 +531,7 @@ import { PipesModule } from 'src/pipes/pipes.module'; BeanPopoverAddComponent, BeanPopoverFreezeComponent, BeanPopoverFrozenListComponent, + BeanPopoverListComponent, BeanArchivePopoverComponent, BeanModalSelectComponent, AssociatedBrewsComponent, diff --git a/src/assets/custom-ion-icons/beanconqueror-expand-active.svg b/src/assets/custom-ion-icons/beanconqueror-expand-active.svg new file mode 100644 index 000000000..c7db982c6 --- /dev/null +++ b/src/assets/custom-ion-icons/beanconqueror-expand-active.svg @@ -0,0 +1 @@ + diff --git a/src/assets/custom-ion-icons/beanconqueror-expand-inactive.svg b/src/assets/custom-ion-icons/beanconqueror-expand-inactive.svg new file mode 100644 index 000000000..a4b978e82 --- /dev/null +++ b/src/assets/custom-ion-icons/beanconqueror-expand-inactive.svg @@ -0,0 +1 @@ + diff --git a/src/assets/custom-ion-icons/beanconqueror-preparation-sanremo-you.svg b/src/assets/custom-ion-icons/beanconqueror-preparation-sanremo-you.svg new file mode 100644 index 000000000..2bee80085 --- /dev/null +++ b/src/assets/custom-ion-icons/beanconqueror-preparation-sanremo-you.svg @@ -0,0 +1,35 @@ + + + + diff --git a/src/assets/custom-ion-icons/beanconqueror-sanremo-you-logo.svg b/src/assets/custom-ion-icons/beanconqueror-sanremo-you-logo.svg new file mode 100644 index 000000000..65a29f37f --- /dev/null +++ b/src/assets/custom-ion-icons/beanconqueror-sanremo-you-logo.svg @@ -0,0 +1,56 @@ + + + + + + + + + + diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 90777414d..d63be163a 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -1273,39 +1273,6 @@ "SHOW_MINUTES": "Minuten anzeigen", "BEANS_UNARCHIVE": "Archivierung aufheben", "TOAST_BEAN_UNARCHIVED_SUCCESSFULLY": "Bean wurde dearchiviert", - "UPDATE_TEXT_TITLE_TITLE": { - "7.4.0": { - "TITLE": "Version 7.4.0: Das ist neu", - "DESCRIPTION": [ - "Bohnen<\/b>", - "Es ist jetzt möglich Kaffeebohnen einzufrieren. Aktiviere diese Funktion in den Einstellungen", - "Bestes und offenes Datum für Bohnen hinzugefügt. Diese Parameter müssen aktiviert werden.", - "Bohnen können nun direkt aus der Auswahlübersicht hinzugefügt werden", - "", - " Thermometer <\/b>", - "Unterstützung des Combustion Inc. Thermometers – Danke für das Gerät!", - "Unterstützung des Meater-Thermometers (nicht Meater 2 oder Meater +) – Danke an Yannick!", - "", - "Wasserbereich<\/b>", - "Pure Coffee Water hinzugefügt", - "Empirical Water hinzugefügt", - "", - "Brühmethoden Tools<\/b>", - "Sortiere deine Zubereitungsmethoden", - "", - "Mühlen<\/b>", - "Mühlenbilder werden in der Übersicht angezeigt", - "", - "Einstellungen<\/b>", - "Sicherheitsmeldung, wenn du die Bewertung für Brühen oder Bohnen geändert hast und das Maximum niedriger ist als ein bereits bewerteter Eintrag", - "", - "Sonstiges<\/b>", - "Der direkte Sekundenfokus beim Öffnen des Timers wurde ausgebaut", - "Einige technische Änderungen im Code", - "Kleine Optimierungen" - ] - } - }, "WATER_TYPE_PURE_COFFEE_WATER": "Pure Coffee Water", "WATER_TYPE_EMPIRICAL_WATER_GLACIAL": "empirical water GLACIAL", "WATER_TYPE_EMPIRICAL_WATER_SPRING": "empirical water SPRING", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b73e29944..f8b9d0341 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1030,7 +1030,8 @@ "TYPE": { "NONE": "None", "XENIA": "Xenia", - "METICULOUS": "Meticulous" + "METICULOUS": "Meticulous", + "SANREMO_YOU": "Sanremo YOU" }, "URL": "Url", "CHOOSE_DEVICE": "Choose device", @@ -1063,6 +1064,14 @@ "CHOOSE_PROFILE": "Choose profile", "SHOT_STARTED": "Shot started", "SHOT_ENDED": "Shot ended" + }, + "TYPE_SANREMO_YOU": { + "TITLE": "Sanremo YOU Machine", + "STOP_AT_WEIGHT": "Stop at weight", + "SELECT_MODE": "Select mode", + "MODE_LISTENING": "Listening", + "MODE_CONTROL": "Control" + } }, "DEVICE_CONNECTION": "Device connection", @@ -1273,39 +1282,6 @@ "SHOW_MINUTES": "Show minutes", "BEANS_UNARCHIVE": "Unarchive", "TOAST_BEAN_UNARCHIVED_SUCCESSFULLY": "Bean has been unarchived", - "UPDATE_TEXT_TITLE_TITLE": { - "7.4.0": { - "TITLE": "Version 7.4.0: What's new", - "DESCRIPTION": [ - "Beans<\/b>", - "Its now possible to freeze your coffee beans, activate this feature in the settings", - "Added best and open date for beans, you need to activate this parameter", - "Beans can now be directly added from the select overview", - "", - "Thermometer<\/b>", - "Support of the Combustion Inc. Thermometer - Thanks for the Device!", - "Support of the Meater Thermometer (not Meater 2 or Meater +) - Thanks to Yannick!", - "", - "Water Section<\/b>", - "Added Pure Coffee Water", - "Added Empirical Water", - "", - "Preparation Tools<\/b>", - "Sort now your preparation tools", - "", - "Grinder<\/b>", - "Grinder images are now shown in the select overview", - "", - "Settings<\/b>", - "Security message if you changed the rating for brews or beans, and the maximum is lower then an already rated entry", - "", - "Other<\/b>", - "Reverted the direct seconds focus when opening the timer", - "Some technical changes in the code", - "Small tweaks" - ] - } - }, "WATER_TYPE_PURE_COFFEE_WATER": "Pure Coffee Water", "WATER_TYPE_EMPIRICAL_WATER_GLACIAL": "empirical water GLACIAL", "WATER_TYPE_EMPIRICAL_WATER_SPRING": "empirical water SPRING", @@ -1352,5 +1328,24 @@ "BEAN_DATA_FROZEN_NOTE": "Frozen notes", "IGNORE_NEGATIVE_VALUES_DESCRIPTION": "Activating this, results into a graph which will be one second behind", "IGNORE_ANOMALY_VALUES_DESCRIPTION": "Activating this, results into a graph which will be one second behind", - "SAVE_LOGFILES_TO_NOTES_FROM_MACHINE": "Save the machine logs when saving a brew" -} \ No newline at end of file + "SAVE_LOGFILES_TO_NOTES_FROM_MACHINE": "Save the machine logs when saving a brew", + "IMPORT_SHOT_FROM_METICULOUS": "Import a shot", + "BEANS_IMPORTED_SUCCESSFULLY_DESCRIPTION": "All found entries in the excel list, have been transformed into new beans, enjoy a tasty brew!", + "IMPORT_UNSUCCESSFULLY": "Import not successful", + "BEAN_LIST": "Bean list", + "IMPORT_ROASTED_BEANS_EXCEL": "Import roasted beans via excel", + "IMPORT_GREEN_BEANS_EXCEL": "Import green beans via excel", + "BEANS_IMPORTED_UNSUCCESSFULLY_WRONG_EXCELFILE": "It looks like that the chosen excel file is corrupted, has wrong data, is wrong structured, or was wrongly chosen. Because of this no beans could be imported.", + "OK": "Ok", + "BEAN_SORT_BEAN_AGE": "Bean age", + "GREEN_BEANS_IMPORTED_SUCCESSFULLY_DESCRIPTION": "All found entries in the excel list, have been transformed into new green beans, have great roasts!", + "PREPARATION_TYPE_SANREMO_YOU": "Sanremo YOU", + "PREPARATION_TYPE_XENIA": "Xenia", + "BEAN_POPUP_YOU_DONT_SEE_EVERYTHING_DESCRIPTION": "You're adding a bean which has variety information in it, but those parameters are not activated. Do you want to activate them now?", + "PAGE_SETTINGS_TAB_BLUETOOTH_PRESSURE_GRAPHS_AXIS": "Define the axes for the pressure graph", + "PAGE_SETTINGS_TAB_BLUETOOTH_PRESSURE_GRAPHS_AXIS_DESCRIPTION": "Set the starting and end size of the axes for pressure", + "PAGE_SETTINGS_TAB_BLUETOOTH_PRESSURE_GRAPHS_AXIS_RANGE": "Pressure axes", + "PAGE_SETTINGS_LANGUAGE_DUTCH": "Dutch", + "DOWNLOAD_IMPORT_EXCEL_TEMPLATES": "Download import templates", + "SHOW_GRAPH": "Graph anzeigen" +} diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 9736cc490..38d1a467d 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -1273,39 +1273,6 @@ "SHOW_MINUTES": "Mostrar minutos", "BEANS_UNARCHIVE": "Desarchivar", "TOAST_BEAN_UNARCHIVED_SUCCESSFULLY": "Café desarchivado", - "UPDATE_TEXT_TITLE_TITLE": { - "7.4.0": { - "TITLE": "Versión 7.4.0: Novedades", - "DESCRIPTION": [ - "Cafés<\/b>", - "Ahora es posible congelar tus cafés, puedes activar esta opción en ajustes.", - "Añadidas fechas de apertura y de consumo óptimo para tus cafés (desactivadas por defecto).", - "Ahora puedes añadir un nuevo café desde el menú de selección de cafés de una preparación.", - "", - "Termómetro<\/b>", - "Añadido soporte para el termómetro Combustion Inc. - Gracias por el aporte!", - "Añadido soporte para el termómetro Meater (no Meater 2 o Meater+) - Gracias a Yannick!", - "", - "Sección de agua<\/b>", - "Añadida agua Pure Coffee Water", - "Añadida agua Empirical Water", - "", - "Métodos de preparación<\/b>", - "Ahora puedes ordenar tus métodos de preparación", - "", - "Molinos<\/b>", - "Ahora se muestran las imágenes de los molinos en el menú de selección.", - "", - "Ajustes<\/b>", - "Se muestra una alerta si cambias la puntuación de una preparación o un café, y el máximo es menor que otra entrada ya puntuada.", - "", - "Otros<\/b>", - "El campo \"segundos\" ya no se selecciona automáticamente al abrir el selector de tiempo.", - "Cambios técnicos en el código.", - "Pequeños ajustes y mejoras." - ] - } - }, "WATER_TYPE_PURE_COFFEE_WATER": "Pure Coffee Water", "WATER_TYPE_EMPIRICAL_WATER_GLACIAL": "empirical water GLACIAL", "WATER_TYPE_EMPIRICAL_WATER_SPRING": "empirical water SPRING", diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index 8f71307e2..17a5ccbe3 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -479,7 +479,7 @@ "CUSTOM_MANAGE_PARAMETERS": "Gérer", "CUSTOM_SORT_PARAMETERS": "Trier", "BREW_PARAMETER_CUSTOMIZE_TITLE": "Paramètres personnalisés pour chaque méthode de préparation", - "BREW_PARAMETER_CUSTOMIZE_DESCRIPTION": "Tu veux choisir des paramètres personnalisés pour chaque méthode de préparation ? Navigues jusqu'à \"Méthodes\", ouvre le menu de la méthode de préparation spécifique et choisissez \"Personnaliser les paramètres\". Là, tu peux choisir quels paramètres seront utilisés pour cette préparation !", + "BREW_PARAMETER_CUSTOMIZE_DESCRIPTION": "Tu veux choisir des paramètres personnalisés pour chaque méthode de préparation ? Navigue jusqu'à \"Méthodes\", ouvre le menu de la méthode de préparation spécifique et choisissez \"Personnaliser les paramètres\". Là, tu peux choisir quels paramètres seront utilisés pour cette préparation !", "BREW_DATA_BREW_QUANTITY_TOOLTIP": "Quantité d'eau (non utilisable pour l'espresso)", "BREW_DATA_COFFEE_FIRST_DRIP_TIME_TOOLTIP": "Premier temps d'égouttage du café (espresso uniquement)", "BREW_DATA_PREPARATION_METHOD_TOOLTIP": "Préparation (uniquement personnalisable lorsqu'elle est active)", @@ -1148,7 +1148,7 @@ }, "SHOT": { "UPLOAD_SUCCESSFULLY": "Infusion téléchargé sur Visualizer.", - "UPLOAD_UNSUCCESSFULLY": "L'infusion n'a pas pu être téléchargé sur Visualizer." + "UPLOAD_UNSUCCESSFULLY": "L'infusion n'a pas pu être téléchargée sur Visualizer." }, "URL": "URL du serveur", "USERNAME": "Nom d'utilisateur", @@ -1273,39 +1273,6 @@ "SHOW_MINUTES": "Afficher les minutes", "BEANS_UNARCHIVE": "Désarchiver", "TOAST_BEAN_UNARCHIVED_SUCCESSFULLY": "Le grain a été désarchivé", - "UPDATE_TEXT_TITLE_TITLE": { - "7.4.0": { - "TITLE": "Version 7.4.0: c'est nouveau", - "DESCRIPTION": [ - "Grains<\/b>", - "Il est possible de congeler des grains de café. Active cette fonction dans les réglages", - "Ajout de la meilleure date et de la date d'ouverture pour les grains. Ces paramètres doivent être activés.", - "Les grains peuvent désormais être ajoutés directement à partir de l’aperçu de la sélection", - "", - " Thermomètre <\/b>", - "Support pour les thermomètres de Combustion Inc. – Merci pour l'appareil !", - "Support du thermomètre Meater (pas Meater 2 ou Meater +) – Merci à Yannick !", - "", - " Zone d'eau <\/b>", - "Ajouté Pure Coffee Water", - "Ajouté Empirical Water", - "", - " Outils de méthode d'infusion <\/b>", - "Trier tes méthodes de préparation", - "", - "Moulins<\/b>", - "Les images des moulins sont affichées dans l'aperçu", - "", - "Paramètres<\/b>", - "Message de sécurité si tu as modifié l'évaluation des infusions ou des grains et que le maximum est inférieur à une entrée déjà évaluée.", - "", - "Autres<\/b>", - "La mise au point directe en secondes lors de l'ouverture de la minuterie a été rétablie", - "Quelques changements techniques dans le code", - "Petites optimisations" - ] - } - }, "WATER_TYPE_PURE_COFFEE_WATER": "Pure Coffee Water", "WATER_TYPE_EMPIRICAL_WATER_GLACIAL": "empirical water GLACIAL", "WATER_TYPE_EMPIRICAL_WATER_SPRING": "empirical water SPRING", diff --git a/src/assets/i18n/id.json b/src/assets/i18n/id.json index fc9969cdf..50594012b 100644 --- a/src/assets/i18n/id.json +++ b/src/assets/i18n/id.json @@ -1273,39 +1273,6 @@ "SHOW_MINUTES": "Tampilkan menit", "BEANS_UNARCHIVE": "Tidak diarsipkan", "TOAST_BEAN_UNARCHIVED_SUCCESSFULLY": "Bean tidak diarsipkan", - "UPDATE_TEXT_TITLE_TITLE": { - "7.4.0": { - "TITLE": "Versi 7.4.0: Apa yang baru", - "DESCRIPTION": [ - "Biji<\/b>", - "Sekarang dimungkinkan untuk mencatat pembekuan biji kopi Anda, aktifkan fitur ini di pengaturan", - "Menambahkan tanggal terbaik dan tanggal membuka kopi, Anda perlu mengaktifkan parameter ini", - "Biji sekarang dapat langsung ditambahkan dari pilihan", - "", - "Thermometer<\/b>", - "Dukungan Termometer Combustion Inc. - Terima kasih untuk Perangkatnya!", - "Dukungan Termometer Meater (bukan Meater 2 atau Meater +) - Terima kasih kepada Yannick!", - "", - "Water Section<\/b>", - "Penambahan Pure Coffee Water", - "Penambahan Empirical Water", - "", - "Preparation Tools<\/b>", - "Jenis alat-alat seduhan dapat diurutkan", - "", - "Grinder<\/b>", - "Gambar grinder sekarang ditampilkan dalam pilihan", - "", - "Pengaturan<\/b>", - "Pesan keamanan jika Anda mengubah peringkat untuk seduhan atau biji kopi, dan nilai maksimumnya lebih rendah dari entri yang sudah diberi peringkat", - "", - "Lain-lain<\/b>", - "Mengembalikan fokus detik langsung saat membuka pengatur waktu", - "Beberapa perubahan teknis pada kode", - "Perubahan kecil" - ] - } - }, "WATER_TYPE_PURE_COFFEE_WATER": "Pure Coffee Water", "WATER_TYPE_EMPIRICAL_WATER_GLACIAL": "empirical water GLACIAL", "WATER_TYPE_EMPIRICAL_WATER_SPRING": "empirical water SPRING", diff --git a/src/assets/i18n/it.json b/src/assets/i18n/it.json index 0face123a..522b9323d 100644 --- a/src/assets/i18n/it.json +++ b/src/assets/i18n/it.json @@ -44,13 +44,13 @@ "PAGE_HOME_DIFFERENT_PREPARATION_METHODS": "metodi di preparazione", "PAGE_HOME_DIFFERENT_MILLS": "macinacaffè", "PAGE_HOME_SUPPORTER": "App Supporter", - "PAGE_HOME_START_BREW": "", + "PAGE_HOME_START_BREW": "Inizia la preparazione", "PAGE_BEANS_LIST_OBTAINABLE": "Disponibili", "PAGE_BEANS_LIST_YOU_GOT_NO_FRESH_BEANS": "Non c'è più caffè!", "PAGE_BEANS_LIST_YOU_GOT_NO_FINISHED_BEANS": "Nessun caffè archiviato", "PAGE_MILL_LIST_NO_MILL_EXISTING": "Nessun macinacaffè", "PAGE_PREPARATION_LIST_NO_PREPARATION_EXISTING": "Nessun metodo di preparazione", - "PAGE_CONTACT_SUGGESTIONS_QUESTIONS_WISHES": "Suggerimenti, domande, bug o richieste?", + "PAGE_CONTACT_SUGGESTIONS_QUESTIONS_WISHES": "Suggerimenti, domande, bugs o richieste?", "PAGE_THANKS_THANKS_FOR_YOUR_SUPPORT": "Grazie per il sostegno!", "PAGE_SETTINGS_LANGUAGE": "Impostazioni della lingua", "PAGE_SETTINGS_LANGUAGE_GERMAN": "Deutsch", @@ -61,8 +61,8 @@ "PAGE_SETTINGS_GENERAL_SETTINGS": "Impostazioni generali", "PAGE_SETTINGS_TRANSFER": "Trasferisci dati", "PAGE_SETTINGS_PRESET_LAST_BREW": "Attiva parametri preimpostati", - "PAGE_SETTINGS_DISPLAY": "", - "PAGE_SETTINGS_DISPLAY_SINGLE_PAGE": "", + "PAGE_SETTINGS_DISPLAY": "Visualizza", + "PAGE_SETTINGS_DISPLAY_SINGLE_PAGE": "Pagina singola", "PAGE_SETTINGS_DISPLAY_PAGING": "Paginazione", "PAGE_SETTINGS_STARTUP_VIEW": "Pagina iniziale", "PAGE_SETTINGS_ANALYTICS_INFORMATION": "Analytics", @@ -76,10 +76,10 @@ "PAGE_STATISTICS_TOTAL_GROUND_BEANS": "Totale caffè macinato", "PAGE_STATISTICS_MONEY_SPENT_FOR_COFFEE": "Soldi spesi in caffè", "PAGE_STATISTICS_DRUNKEN_BREWS": "Numero totale preparazioni", - "PAGE_STATISTICS_BREW_PROCESSES": "Preparazioni", + "PAGE_STATISTICS_BREW_PROCESSES": "Preparazioni totali", "PAGE_STATISTICS_DRUNKEN_QUANTITY": "Quantità consumata", "PAGE_STATISTICS_BEAN_WEIGHT_USED": "Totale caffè macinato", - "PAGE_BREW_TEXT_INFORMATION_FROM_ROASTER": "", + "PAGE_BREW_TEXT_INFORMATION_FROM_ROASTER": "Info sulla torrefazione", "PAGE_ABOUT_NO_VERSION_AVAILABLE": "Nessuna versione disponibile", "PAGE_ABOUT_APP_VERSION": "Versione App", "PAGE_LICENCES_WEBSITE": "Sito web", @@ -1273,39 +1273,6 @@ "SHOW_MINUTES": "Mostra minuti", "BEANS_UNARCHIVE": "Annulla l'archiviazione", "TOAST_BEAN_UNARCHIVED_SUCCESSFULLY": "Il caffè è stato disarchiviato", - "UPDATE_TEXT_TITLE_TITLE": { - "7.4.0": { - "TITLE": "Versione 7.4.0: Cosa c'è di nuovo", - "DESCRIPTION": [ - "Caffè<\/b>", - "Ora è possibile tenere traccia del caffè congelato, puoi attivare questa funzione nelle impostazioni", - "Aggiunta della data suggerita e di apertura per i caffè, è necessario attivare questo parametro nelle impostazioni", - "Ora è possibile aggiungere del caffè direttamente dalla selezione", - "", - " Termometro <\/b>", - "Supporto del termometro Combustion Inc. - Grazie per il dispositivo!", - "Supporto del termometro Meater (non Meater 2 o Meater +) - Grazie a Yannick!", - "", - "Sezione Acqua<\/b>", - "Aggiunta l'acqua Pure Coffee Water", - "Aggiunta l'acqua Empirical Water", - "", - "Accessori preparazione<\/b>", - "Ordina ora i tuoi strumenti di preparazione", - "", - "Macinacaffè<\/b>", - "Le immagini del macinacaffè sono mostrate nella selezione", - "", - "Impostazioni<\/b>", - "Messaggio di sicurezza se hai modificato la valutazione e il massimo è inferiore a cioè che è già valutato", - "", - "Altro<\/b>", - "Rimossa la recente aggiunta di auto-focus sui secondi del timer della preparazione", - "Piccole migliorie nel codice", - "Piccole migliorie" - ] - } - }, "WATER_TYPE_PURE_COFFEE_WATER": "Pure Coffee Water", "WATER_TYPE_EMPIRICAL_WATER_GLACIAL": "empirical water GLACIAL", "WATER_TYPE_EMPIRICAL_WATER_SPRING": "empirical water SPRING", diff --git a/src/assets/i18n/nl.json b/src/assets/i18n/nl.json new file mode 100644 index 000000000..31e2cd3e7 --- /dev/null +++ b/src/assets/i18n/nl.json @@ -0,0 +1,1323 @@ +{ + "NAV_MENU": "Menu", + "NAV_HOME": "Thuis", + "NAV_SETTINGS": "Instellingen", + "NAV_BREWS": "Brouwsels", + "NAV_BEANS": "Bonen", + "NAV_PREPARATION": "Methoden", + "NAV_MILL": "Malers", + "NAV_ABOUT_US": "Over ons", + "NAV_CONTACT": "Contact", + "NAV_PRIVACY": "Privacy", + "NAV_CREDITS": "Credits", + "NAV_TERMS": "Algemene voorwaarden", + "NAV_THANKS": "Bedankt!", + "NAV_LICENCES": "OSS-licenties", + "NAV_STATISTICS": "Statistieken", + "NAV_IMPRESSUM": "Impressum", + "NAV_COOKIE": "Cookies", + "NAV_LOGS": "Logboeken", + "NAV_BREW_PARAMS": "Brouwparameters", + "NAV_INFORMATION_TO_APP": "Over Beanconqueror", + "NAV_WATER_SECTION": "Water", + "NAV_HELPER": "Calculaties", + "POPOVER_BREWS_OPTION_REPEAT": "Herhalen", + "POPOVER_BREWS_OPTION_DETAIL": "Details", + "DETAIL": "Details", + "POPOVER_BREWS_OPTION_EDIT": "Bewerking", + "POPOVER_BREWS_OPTION_DELETE": "Verwijder", + "POPOVER_BREWS_OPTION_PHOTO_GALLERY": "Fotogalerij", + "POPOVER_BREWS_OPTION_CUPPING": "Cupping", + "POPOVER_BREWS_OPTION_MAP_COORDINATES": "Toon op kaart", + "POPOVER_BREWS_OPTION_FAST_REPEAT": "Snel brouwen herhalen", + "PAGE_BREWS_NO_ENTRIES": "Nog geen brouwsels toegevoegd", + "PAGE_BREWS_NO_ARCHIVED_ENTRIES": "Je hebt nog geen brouwsels gearchiveerd", + "CANT_START_NEW_BREW_TITLE": "Er ontbreekt hier iets...", + "CANT_START_NEW_BREW_DESCRIPTION": "Om te beginnen, maak een beschrijving voor één type bonen, één zetmethode en één grinder. Gebruik het menu om naar de verschillende categorieën te navigeren om deze informatie toe te voegen.", + "PAGE_HOME_WELCOME_GREETINGS": "Leuk dat je er bent!", + "PAGE_HOME_TOTAL_BREW": "Brouwen", + "PAGE_HOME_TOTAL_BREWS": "Brouwsels", + "PAGE_HOME_BEAN_EXPLORED": "Bonen onderzocht", + "PAGE_HOME_BEANS_EXPLORED": "Bonen onderzocht", + "PAGE_HOME_LAST_BREWS": "Laatste brouwsels", + "PAGE_HOME_LAST_BREW": "Laatste brouwsel", + "PAGE_HOME_DIFFERENT_PREPARATION_METHODS": "Verschillende bereidingsmethoden", + "PAGE_HOME_DIFFERENT_MILLS": "Verschillende malers", + "PAGE_HOME_SUPPORTER": "App Supporter", + "PAGE_HOME_START_BREW": "Begin met brouwen", + "PAGE_BEANS_LIST_OBTAINABLE": "Beschikbaar", + "PAGE_BEANS_LIST_YOU_GOT_NO_FRESH_BEANS": "Je hebt geen verse bonen meer!", + "PAGE_BEANS_LIST_YOU_GOT_NO_FINISHED_BEANS": "Je hebt geen gearchiveerde bonen.", + "PAGE_MILL_LIST_NO_MILL_EXISTING": "Je hebt nog geen grinders toegevoegd.", + "PAGE_PREPARATION_LIST_NO_PREPARATION_EXISTING": "Je hebt nog geen zetmethodes toegevoegd.", + "PAGE_CONTACT_SUGGESTIONS_QUESTIONS_WISHES": "Suggesties, vragen, bugs of verzoeken?", + "PAGE_THANKS_THANKS_FOR_YOUR_SUPPORT": "Bedankt voor je steun!", + "PAGE_SETTINGS_LANGUAGE": "Taalinstellingen", + "PAGE_SETTINGS_LANGUAGE_GERMAN": "Duits", + "PAGE_SETTINGS_LANGUAGE_ENGLISH": "Engels", + "PAGE_SETTINGS_LANGUAGE_SPANISH": "Spaans", + "PAGE_SETTINGS_LANGUAGE_TURKISH": "Turks", + "PAGE_SETTINGS_LANGUAGE_CHINESE": "Chinees", + "PAGE_SETTINGS_GENERAL_SETTINGS": "Algemene instellingen", + "PAGE_SETTINGS_TRANSFER": "Gegevens overdragen", + "PAGE_SETTINGS_PRESET_LAST_BREW": "Vooraf ingestelde waarden?", + "PAGE_SETTINGS_DISPLAY": "Weergave", + "PAGE_SETTINGS_DISPLAY_SINGLE_PAGE": "Eén pagina", + "PAGE_SETTINGS_DISPLAY_PAGING": "Paging", + "PAGE_SETTINGS_STARTUP_VIEW": "Startpagina", + "PAGE_SETTINGS_ANALYTICS_INFORMATION": "Analytics", + "PAGE_SETTINGS_ANALYTICS_INFORMATION_TOOLTIP": "Druk op i voor meer informatie", + "PAGE_SETTINGS_TRACK_BREW_COORDINATES": "Geolocatie van brouwsel opslaan", + "PAGE_SETTINGS_FAST_REPEAT": "Snel herhalen", + "PAGE_SETTINGS_TRACK_CAFFEINE_CONSUMPTION": "bewaar cafeïneconsumptie", + "PAGE_SETTINGS_WAKE_LOCK": "Houd het display actief tijdens het brouwen", + "PAGE_SETTINGS_CURRENCY": "Valuta", + "PAGE_STATISTICS_DIFFERENT_PREPARATION_METHOD": "Bereidingsmethoden", + "PAGE_STATISTICS_TOTAL_GROUND_BEANS": "Totaal gemalen bonen", + "PAGE_STATISTICS_MONEY_SPENT_FOR_COFFEE": "Geld uitgegeven aan bonen", + "PAGE_STATISTICS_DRUNKEN_BREWS": "Totaal aantal brouwsels", + "PAGE_STATISTICS_BREW_PROCESSES": "Totaal brouwsels", + "PAGE_STATISTICS_DRUNKEN_QUANTITY": "Verbruikte hoeveelheid", + "PAGE_STATISTICS_BEAN_WEIGHT_USED": "Totaal gemalen bonen", + "PAGE_BREW_TEXT_INFORMATION_FROM_ROASTER": "Informatie over de brander", + "PAGE_ABOUT_NO_VERSION_AVAILABLE": "Geen Versie beschikbaar", + "PAGE_ABOUT_APP_VERSION": "App Versie", + "PAGE_LICENCES_WEBSITE": "Website", + "BEAN_DATA_ROAST_NAME": "Mate van branding", + "BEAN_DATA_CUSTOM_ROAST_NAME": "Aangepaste mate van branding", + "BEAN_DATA_ROASTING_DATE": "Brand datum", + "BEAN_DATA_ROASTER": "Brander", + "BEAN_DATA_VARIETY": "Variëteit", + "BEAN_DATA_PROCESSING": "Verwerking", + "BEAN_DATA_COUNTRY": "Land", + "BEAN_DATA_MIX": "Melange", + "BEAN_DATA_AROMATICS": "Smaakprofiel", + "BEAN_DATA_WEIGHT": "Gewicht", + "BEAN_DATA_COST": "kosten", + "BEAN_DATA_NAME": "Naam", + "BEAN_DATA_REGION": "Regio", + "BEAN_DATA_FARM": "Boerderij", + "BEAN_DATA_FARMER": "Boer", + "BEAN_DATA_ELEVATION": "Elevatie", + "BEAN_DATA_HARVEST_TIME": "Geoogst", + "BEAN_DATA_PERCENTAGE": "Percentage", + "BEAN_DATA_CERTIFICATION": "Bonencertificering", + "BEAN_DATA_ROASTING_TYPE": "type branding", + "BEAN_DATA_DECAFFEINATED": "Cafeïnevrij", + "BEAN_DATA_URL": "Website", + "BEAN_DATA_EAN": "EAN \/ Artikelnummer", + "BEAN_DATA_PURCHASING_PRICE": "Aankoopprijs", + "BEAN_DATA_FOB_PRICE": "FOB-prijs", + "BEAN_DATA_CUPPING_POINTS": "Cupping-punten", + "BREW_DATA_CUSTOM_BREW_TIME": "Aangepaste brouwtijd", + "BREW_CREATION_DATE": "Aanmaakdatum", + "REPEAT": "Herhalen", + "EDIT": "Bewerken", + "DELETE": "Verwijderen", + "FINISHED": "Archief", + "NOTES": "Notities", + "ADD_PHOTO": "Foto toevoegen", + "CANCEL": "Annuleren", + "GENERATE": "Genereren", + "SAVE": "Opslaan", + "ADD_SOMETHING": "Toevoegen", + "CONTACT": "Contact", + "NAME": "Naam", + "IMPORT": "import", + "EXPORT": "Export", + "VIEW": "Weergave", + "ARCHIVE": "archief", + "CURRENT": "huidig", + "BACK": "Terug", + "CLOSE": "sluiten", + "DAY": "Dag", + "BREW_DATA_TEMPERATURE_TIME": "Temperatuur Tijd", + "BREW_DATA_SURF_TIME": "Surftijd", + "BREW_DATA_TIME": "Tijd", + "BREW_DATA_GRIND_SIZE": "Maalinstelling", + "BREW_DATA_GRIND_WEIGHT": "Gemalen koffie (gr)", + "BREW_DATA_IN_OUT_BR": "In\/Uit (BR)", + "BREW_DATA_NOTES": "Notities", + "BREW_DATA_PREPARATION_METHOD": "Bereidingswijze", + "BREW_DATA_MILL": "Maler", + "BREW_DATA_MILL_SPEED": "Maalsnelheid (rpm)", + "BREW_DATA_MILL_TIMER": "Maaltijd", + "BREW_DATA_BREW_QUANTITY": "Hoeveelheid water", + "BREW_DATA_BEAN_TYPE": "Boon type", + "BREW_DATA_BREW_TEMPERATURE": "Brouwtemperatuur", + "BREW_DATA_PRESSURE_PROFILE": "Profiel", + "BREW_DATA_COFFEE_TYPE": "Type koffie", + "BREW_DATA_COFFEE_CONCENTRATION": "Koffie Concentratie", + "BREW_DATA_COFFEE_FIRST_DRIP_TIME": "Eerste druppeltijd", + "BREW_DATA_COFFEE_BLOOMING_TIME": "Bloeitijd \/ Pre-infusie", + "BREW_DATA_ATTACHMENTS": "Bijlagen \/ Foto's", + "BREW_DATA_RATING": "Beoordeling", + "BREW_DATA_CALCULATED_COFFEE_BREW_TIME": "Koffiezettijd", + "BREW_DATA_TDS": "Totaal opgeloste vaste stoffen %", + "BREW_DATA_CALCULATED_EXTRACTION_YIELD": "Extractie Opbrengst %", + "BREW_INFORMATION_BREW_RATIO": "Brouw Verhouding", + "BREW_INFORMATION_BEAN_AGE": "Boon leeftijd", + "BREW_INFORMATION_BREW_QUANTITY_TYPE_NAME": "Hoeveelheidstype", + "BREW_DATA_TDS_EY": "TDS \/ %EY", + "BREW_DATA_BREW_BEVERAGE_QUANTITY": "Hoeveelheid drank", + "BREW_DATA_PREPARATION_METHOD_TOOL": "bereidings gereedschappen", + "BREW_DATA_WATER": "Water", + "BREW_DATA_BEAN_WEIGHT_IN": "Bonengewicht (g)", + "BREW_DATA_VESSEL": "Serveerkan", + "BREW_DATA_VESSEL_WEIGHT": "Serveerkan gewicht", + "BREW_DATA_VESSEL_NAME": "Serveerkan naam", + "BREW_DATA_FLAVOR": "Smaak", + "BREW_DATA_FLOW_PROFILE": "Stroom", + "ONE_DAY": "Dag", + "DAYS": "Dagen", + "ONE_HOUR": "uur", + "HOURS": "uren", + "ONE_MINUTE": "minuut", + "MINUTES": "minuten", + "WITHOUT_COFFEE": "zonder koffie", + "NOT_FOUND": "Niet gevonden", + "INVALID_FILE_FORMAT": "Ongeldig bestandsformaat", + "FILE_NOT_FOUND_INFORMATION": "Bestand niet gevonden", + "ERROR_ON_FILE_READING": "Fout bij het lezen van bestandsgegevens", + "IMPORT_SUCCESSFULLY": "Importeren succesvol", + "IMPORT_UNSUCCESSFULLY_DATA_NOT_CHANGED": "Import mislukt, er zijn geen gegevens gewijzigd", + "INVALID_FILE_DATA": "Ongeldige bestandsinhoud", + "DOWNLOADED": "Gedownload", + "FILE_DOWNLOADED_SUCCESSFULLY": "Bestand ' {{fileName}} ' is succesvol gedownload naar uw downloadmap!", + "NO": "Nee", + "YES": "Ja", + "SURE_QUESTION": "Weet je het zeker?", + "DELETE_BREW_QUESTION": "Brouwsel verwijderen?", + "DELETE_BEAN_QUESTION": "Boon verwijderen? Alle bijbehorende brouwsels worden ook verwijderd!", + "DELETE_GREEN_BEAN_QUESTION": "Groene bonen verwijderen? Alle bijbehorende geroosterde bonen, evenals brouwsels worden verwijderd!", + "DELETE_MILL_QUESTION": "Grinder verwijderen? Alle bijbehorende brouwsels worden ook verwijderd!", + "DELETE_PREPARATION_METHOD_QUESTION": "Bereidingswijze verwijderen? Alle bijbehorende brouwsels worden ook verwijderd!", + "DELETE_PREPARATION_TOOL_QUESTION": "Bereidingstool verwijderen? Alle brouwsels die aan deze tool zijn gekoppeld, worden bijgewerkt.", + "APP_COULD_NOT_STARTED_CORRECTLY_BECAUSE_MISSING_FILESYSTEM": "App kon niet correct worden gestart vanwege ontbrekend bestandssysteem", + "CARE": "Zorg", + "ERROR_OCCURED": "Er is een fout opgetreden", + "CSV_FILE_NOT_DOWNLOADED": "CSV-bestand kon niet worden gedownload!", + "CSV_FILE_DOWNLOADED_SUCCESSFULLY": "CSV-bestand ' {{fileName}} ' is succesvol gedownload naar uw downloadmap!", + "ADD_BREW": "Voeg brouwsel toe", + "CHOOSE": "Kies", + "CHOOSE_PHOTO_OR_LIBRARY": "Maak een foto of kies uit de fotogalerij.", + "RECORD": "Maak een foto", + "PAGE_BEAN_INFORMATION": "Boon informatie", + "PAGE_BEAN_INFORMATION_GOOD_BREWS": "Goed", + "PAGE_BEAN_INFORMATION_BAD_BREWS": "Slecht", + "PAGE_BEAN_INFORMATION_COUNT_BREWS": "Totaal aantal brouwsels", + "INFORMATION": "Informatie", + "PAGE_BEAN_BREW_CHART_TITLE": "Brouwoverzicht voor deze boon", + "PAGE_BEAN_INFORMATION_AWESOME_BREWS": "Geweldig", + "PAGE_BEAN_INFORMATION_NORMAL_BREWS": "Normaal", + "PAGE_BEAN_INFORMATION_NOT_RATED_BREWS": "Niet beoordeeld", + "PAGE_PREPARATION_INFORMATION_BREWS_DONE": "Brouwsels met deze bereidingswijze", + "PAGE_PREPARATION_INFORMATION_BREWED_QUANTITY": "Gebrouwen hoeveelheid", + "PAGE_PREPARATION_INFORMATION_GRIND_WEIGHT": "Verbruikt gewicht van bonen", + "PAGE_PREPARATION_INFORMATION_TIME_SPENT_BREWING": "Totale bereidingstijd", + "PAGE_PREPARATION_INFORMATION": "Informatie over de bereiding", + "SECONDS": "Seconden", + "PAGE_MILL_INFORMATION": "Maler informatie", + "PAGE_MILL_INFORMATION_BREWS_DONE": "Brouwsels met deze maler", + "PAGE_MILL_INFORMATION_GRIND_WEIGHT": "Verbruikt gewicht van bonen", + "PAGE_HELPER_WATER_HARDNESS": "Waterhardheid", + "PAGE_HELPER_WATER_HARDNESS_CA_CONTENTS": "Calciumgehalte mg\/l", + "PAGE_HELPER_WATER_HARDNESS_MG_CONTENTS": "Magnesiumgehalte mg\/l", + "PAGE_HELPER_WATER_HARDNESS_GERMAN_HARDNESS": "Duitse hardheidsgraad", + "PAGE_HELPER_WATER_HARDNESS_TOTAL_HARDNESS": "Totale hardheid", + "PAGE_HELPER_BREW_RATIO": "Brouw Verhouding", + "PAGE_HELPER_BREW_RATIO_GROUND_COFFEE": "Gemalen koffie (gr)", + "PAGE_HELPER_BREW_RATIO_WATER": "Vloeistof (gr\/ml)", + "PAGE_HELPER_BREW_RATIO_CALCULATED": "Berekende brouwverhouding", + "PAGE_SETTINGS_SHOW_ARCHIVED_BREWS": "Gearchiveerde brouwsels weergeven", + "PAGE_SETTINGS_SHOW_ARCHIVED_BEANS": "Gearchiveerde bonen weergeven", + "PAGE_SETTINGS_SHOW_ARCHIVED_GREEN_BEANS": "Toon gearchiveerde groene bonen", + "CUPPING_SCORE": "Score", + "CUPPING_SCORE_DRY_FRAGRANCE": "Droge geur", + "CUPPING_SCORE_WET_AROMA": "Nat aroma", + "CUPPING_SCORE_BRIGHTNESS": "Helderheid", + "CUPPING_SCORE_FLAVOR": "Smaak", + "CUPPING_SCORE_BODY": "Body", + "CUPPING_SCORE_FINISH": "Afwerking", + "CUPPING_SCORE_SWEETNESS": "Zoetheid", + "CUPPING_SCORE_CLEAN_CUP": "Clean Cup", + "CUPPING_SCORE_COMPLEXITY": "Complexiteit", + "CUPPING_SCORE_UNIFORMITY": "Uniformiteit", + "CUPPING_SCORE_CUPPERS_CORRECTION": "Cuppers-correctie", + "CUPPING_SCORE_DRY_FRAGRANCE_TOOLTIP": "Verwijst naar het aroma van de droge gemalen koffie voordat er heet water aan wordt toegevoegd.", + "CUPPING_SCORE_WET_AROMA_TOOLTIP": "De geur van nat koffiedik, nadat er heet water aan is toegevoegd.", + "CUPPING_SCORE_BRIGHTNESS_TOOLTIP": "Zuurgraad is de smaak van scherpe hoge tonen in de koffie, veroorzaakt door een set van chlorogeen, citroenzuur, kininezuur, azijnzuur en andere, voornamelijk waargenomen in de voorkant van de mond en op de tong. (Het is een goede kwaliteit; niet gerelateerd aan bitterheid in koffie, en niet direct verantwoordelijk voor maagklachten). Zuurgraad wordt gewaardeerd door veel kopers, en is direct gerelateerd aan de kwaliteit van de kop, aangezien zuurgraad het product is van aanplantingen op grote hoogte.", + "CUPPING_SCORE_FLAVOR_TOOLTIP": "Dit is de algehele indruk in de mond, inclusief alle andere beoordelingen. Er zijn 4 \"primaire smaak\"-groeperingen (zuur, zoet, zout, bitter) en veel \"secundaire smaken\".", + "CUPPING_SCORE_BODY_TOOLTIP": "Vaak \"mondgevoel\" genoemd, is body het gevoel van gewicht en dikte van de gezette koffie, veroorzaakt door het percentage oplosbare vaste stoffen in de kop, inclusief alle organische verbindingen die worden geëxtraheerd (de zetmethode en de hoeveelheid gemalen koffie die wordt gebruikt, hebben hier grote invloed op). We beoordelen Body op een lagere schaal omdat licht gebodyde koffiesoorten zeker niet slecht zijn en in sommige origines past de lichtere body het beste bij het algehele karakter van de kop.", + "CUPPING_SCORE_FINISH_TOOLTIP": "De aanhoudende of opkomende smaken die komen nadat de mond is schoongemaakt. Dit omvat de tijd dat de koffie je mond verlaat tot minuten daarna...een reden dat je veel cuppers zult vinden die hun nasmaakscores herzien terwijl ze een minuut of twee later nog steeds een positieve smaak ervaren.", + "CUPPING_SCORE_SWEETNESS_TOOLTIP": "Zoetheid is bijna altijd een gewenste kwaliteit in koffie, zelfs als het op eufemistische wijze wordt beschreven, zoals \"rustieke zoetheid\" of \"bitterzoetheid\". U zult merken dat verfijnde zoetheid (denk aan Europees gebak, fijn snoepgoed, witte suiker, pure zoetheid) hoog scoort, evenals complexe zoetheid van fruitsuikers (fructose). Moutige zoetheid (maltose) is minder traditioneel, maar wel wenselijk en honing kan variëren van heel puur en schoon tot complex, rustiek, bijna gistachtig. Kortom, als zoetheid een sleutel is tot de kop, zal het goed worden beoordeeld.", + "CUPPING_SCORE_CLEAN_CUP_TOOLTIP": "Let op dat \"clean cup\" niet letterlijk betekent dat er geen vuil op de koffie zit. Het gaat alleen om smaak en rauwe, funky koffies die \"onrein\" zijn en de smaak kan ook heel wenselijk zijn, zoals nat-gepelde Indonesische koffies uit Sumatra, of droog verwerkte Ethiopische en Jemenitische types.", + "CUPPING_SCORE_COMPLEXITY_TOOLTIP": "Complexiteit complimenteert de scores voor \"smaak\" en \"afwerking\", om een veelheid of gelaagdheid van veel smaken te communiceren. Het betekent dat er veel te ontdekken valt in de kop. Aan de andere kant kunnen simpele koffies een opluchting zijn na overmatige blootstelling aan veel krachtige, intense, complexe koffies.", + "CUPPING_SCORE_UNIFORMITY_TOOLTIP": "Uniformiteit verwijst naar verschillen tussen kopjes. Koffiesoorten die met een droog proces zijn bereid, kunnen van nature minder uniform zijn dan koffiesoorten die met een nat proces zijn bereid. We zouden nooit een partij met fantastische smaken vermijden als deze af en toe afwijkt. Dit wordt gescoord tijdens het cuppingprotocol, waarbij meerdere kopjes worden gemaakt van elke partij die wordt beoordeeld.", + "CUPPING_SCORE_CUPPERS_CORRECTION_TOOLTIP": "Dit is aangepast van het SCAA-systeem en de Cup of Excellence-score (soms noemen ze het \"Overall Points\"). Het stelt een cupper in staat om ervoor te zorgen dat de totale score de algehele indruk van de cup correct weergeeft. U zou deze aanpak kunnen bekritiseren en het \"vervalsen\" van het totaal kunnen beschouwen. In zekere zin zou u gelijk hebben ... maar het zou veel erger zijn om de categoriescores te veranderen om het gewenste totaal te bereiken (om een koffie een 9 te geven voor zuurgraad terwijl u weet dat het een 7 is), of omgekeerd om een koffie die absoluut een 90 verdient, op 84 te laten eindigen. Het specifieke Cupper's Correction-nummer doet er niet toe, of het nu een 5 of een 8 is ... het idee is dat de totale score een correcte indruk geeft van de kwaliteit van de koffie.", + "CUPPING_SCORE_TOOLTIP": "100-95 = Verbazingwekkend, 90-94 = Uitstekend, 85-89 = Zeer goed, 80-84 = Goed, 75-79 = Redelijk, 70-74 = Slecht", + "DETAIL_BREW": "Brouw details", + "DETAIL_BEAN": "Boon details", + "DETAIL_MILL": "Maler details", + "DETAIL_PREPARATION": "Bereidings details", + "EDIT_BREW": "Bewerk brouwsel", + "ADD_BEAN": "Boon toevoegen", + "EDIT_BEAN": "Bewerken boon", + "ADD_PREPARATION": "Bereidingsmethode toevoegen", + "EDIT_PREPARATION": "Bewerk bereidingsmethode", + "ADD_MILL": "Voeg maler toe", + "EDIT_MILL": "Maler bewerken", + "USE_FILTER": "Filter toepassen", + "RESET_FILTER": "Filter resetten", + "COFFEE_GRAMS_GRINDED": "Gram gemalen", + "BEANS_USED": "Bonen geconsumeerd", + "BREW_HEADER_BEFORE_BREW": "Voor het brouwen", + "BREW_HEADER_WHILE_BREW": "Tijdens het brouwen", + "BREW_HEADER_AFTER_BREW": "Na het brouwen", + "BREW_HEADER_CUPPING": "Proeven", + "BEANS_CONSUMED": "Archieveer", + "NAV_MANAGE_PARAMETERS": "Parameters beheren", + "NAV_SORT_PARAMETERS": "Sorteer parameters", + "NAV_DEFAULT_PARAMETERS": "Standaardparameters definiëren", + "PAGE_SORT_PARAMETERS_DESCRIPTION": "Versleep parameters om te bepalen in welke volgorde ze worden weergegeven.", + "PAGE_MANAGE_PARAMETERS_DESCRIPTION": "Geef aan welke parameters moeten worden weergegeven bij het bewerken van brouwinformatie.", + "PAGE_DEFAULT_PARAMETERS_DESCRIPTION": "Markeer welke parameters standaard moeten worden ingesteld op de laatst gebruikte waarde.", + "SORT_PARAMETERS_BEFORE": "Voor het brouwen", + "SORT_PARAMETERS_MEANWHILE": "Tijdens het brouwen", + "SORT_PARAMETERS_AFTER": "Na het brouwen", + "MORE_INFORMATION": "Meer informatie", + "UNDERSTOOD": "Begrepen", + "WELCOME_PAGE_ACTIVATE_ANALYTICS_TITLE": "Analyse en tracking", + "WELCOME_PAGE_ACTIVATE_ANALYTICS_DESCRIPTION": "We willen de app, de website en onze toekomstige diensten voor u voortdurend verbeteren. Om dit te doen, moeten we wat gegevens bijhouden over hoe u de app en de functies ervan gebruikt. Maar we beloven dat we nooit persoonlijke gegevens zullen bijhouden. Om deze beloften waar te maken, gebruiken we Matomo, een open source service gericht op gegevensbeveiliging en privacy die op onze eigen server wordt gehost - dit zorgt ervoor dat alleen wij eigenaar zijn van de gegevens. Onze website biedt alle informatie over de parameters die we bijhouden en bovendien kunt u de broncode bekijken die 100% open source is. Heeft u vragen? Neem dan gerust contact met ons op.", + "ANALYTICS_INFORMATION_TITLE": "Analyse en tracking", + "ANALYTICS_INFORMATION_DESCRIPTION": "Zoals u weet, is de veiligheid van uw gegevens en uw privacy onze topprioriteit. Daarom zijn we overgestapt van Google Analytics naar de open source service Matomo, die zich richt op gegevensbeveiliging en privacy. Deze service wordt gehost op onze eigen server. Dit betekent dat wij volledig eigenaar zijn van de gegevens. De bijgehouden parameters zijn niet gewijzigd en we beloven nog steeds dat we nooit persoonlijke gegevens zullen bijhouden. Op onze website vindt u alle informatie over de parameters die we bijhouden. Bovendien kunt u de broncode bekijken, die 100% open source is. Heeft u vragen? Neem dan gerust contact met ons op.", + "ACTIVATE": "Activeren", + "DO_NOT_ACTIVE": "Niet activeren", + "WELCOME_PAGE_BEAN_TITLE": "Boon", + "WELCOME_PAGE_BEAN_DESCRIPTION": "Koffie zetten zonder bonen is een beetje lastig. Voeg je eerste boon toe om te beginnen!", + "WELCOME_PAGE_BEAN_ADD": "Boon toevoegen", + "SKIP": "Overslaan", + "WELCOME_PAGE_PREPARATION_TITLE": "Bereidingswijze", + "WELCOME_PAGE_PREPARATION_DESCRIPTION": "V60, Aeropress, Espresso - er zijn veel manieren om koffie te zetten. Voeg er minstens één toe.", + "WELCOME_PAGE_PREPARATION_ADD": "Bereidingswijze toevoegen", + "WELCOME_PAGE_MILL_TITLE": "Maler", + "WELCOME_PAGE_MILL_DESCRIPTION": "Bijna klaar, maar je hebt iets nodig om je bonen te malen! Voeg minstens één maler toe.", + "WELCOME_PAGE_MILL_ADD": "Voeg Maler toe", + "WELCOME_PAGE_TITLE": "Welkom!", + "WELCOME_PAGE_BEAN_HEADLINE": "Eerste boon", + "WELCOME_PAGE_PREPARATION_HEADLINE": "Bereidingswijze toevoegen", + "WELCOME_PAGE_MILL_HEADLINE": "Eerste Maler", + "WELCOME_PAGE_LETS_START_HEADLINE": "Nu kunnen we beginnen met brouwen!", + "WELCOME_PAGE_LETS_START_TITLE": "Nu kunnen we beginnen met brouwen!", + "WELCOME_PAGE_LETS_START_DESCRIPTION": "Gefeliciteerd, je bent klaar om de beste koffie van je leven te zetten. Veel plezier en verspreid de liefde voor goede koffie!", + "PREPARATION_TYPE": "Bereidingsmethode", + "PREPARATION_TYPE_NAME": "Naam", + "ARCHIVED": "Gearchiveerd", + "PAGE_SETTINGS_SHOW_ARCHIVED_PREPARATIONS": "Toon gearchiveerde bereidingsmethoden", + "PAGE_SETTINGS_SHOW_ARCHIVED_MILLS": "Toon gearchiveerde malers", + "PAGE_MILL_LIST_NO_ARCHIVED_MILL_EXISTING": "Er zijn nog geen malers gearchiveerd.", + "PAGE_PREPARATION_LIST_NO_ARCHIVED_PREPARATION_EXISTING": "Je hebt geen gearchiveerde bereidingswijzen.", + "TOAST_BREW_ADDED_SUCCESSFULLY": "Brouwsel succesvol toegevoegd", + "TOAST_BREW_REPEATED_SUCCESSFULLY": "Brouwsel succesvol herhaald", + "TOAST_BEAN_ADDED_SUCCESSFULLY": "Boon succesvol toegevoegd", + "TOAST_MILL_ADDED_SUCCESSFULLY": "Maler succesvol toegevoegd", + "TOAST_PREPARATION_ADDED_SUCCESSFULLY": "Bereidingswijze succesvol toegevoegd", + "TOAST_WATER_ADDED_SUCCESSFULLY": "Water succesvol toegevoegd", + "TOAST_BREW_DELETED_SUCCESSFULLY": "Brouwsel is verwijderd", + "TOAST_BEAN_DELETED_SUCCESSFULLY": "Boon is verwijderd", + "TOAST_GREEN_BEAN_DELETED_SUCCESSFULLY": "Groene boon is verwijderd", + "TOAST_MILL_DELETED_SUCCESSFULLY": "Maler is verwijderd", + "TOAST_WATER_DELETED_SUCCESSFULLY": "Water is verwijderd", + "TOAST_PREPARATION_DELETED_SUCCESSFULLY": "Bereidingswijze is verwijderd", + "TOAST_BREW_EDITED_SUCCESSFULLY": "Brouwsel is bewerkt", + "TOAST_BEAN_EDITED_SUCCESSFULLY": "Boon is bewerkt", + "TOAST_MILL_EDITED_SUCCESSFULLY": "Maler is bewerkt", + "TOAST_PREPARATION_EDITED_SUCCESSFULLY": "Bereiding is bewerkt", + "TOAST_WATER_EDITED_SUCCESSFULLY": "Water is bewerkt", + "TOAST_BEAN_ARCHIVED_SUCCESSFULLY": "Boon is gearchiveerd", + "TOAST_MILL_ARCHIVED_SUCCESSFULLY": "Maler is gearchiveerd", + "TOAST_PREPARATION_ARCHIVED_SUCCESSFULLY": "Bereidingsmethode is gearchiveerd", + "TOAST_WATER_ARCHIVED_SUCCESSFULLY": "Water is gearchiveerd", + "BEAN_WEIGHT_ALREADY_USED": "{{gramUsed}} g van {{gramTotal}} g ( {{leftOver}} g)", + "PREPARATION_TYPE_CUSTOM_PREPARATION": "Aangepaste bereidingsmethode", + "PREPARATION_TYPE_AEROPRESS": "Aeropress", + "PREPARATION_TYPE_V60": "V60", + "PREPARATION_TYPE_CHEMEX": "Chemex", + "PREPARATION_TYPE_BIALETTI": "Bialetti", + "PREPARATION_TYPE_PORTAFILTER": "Espresso machine", + "PREPARATION_TYPE_KALITA_WAVE": "Kalita Wave", + "PREPARATION_TYPE_FRENCH_PRESS": "French Press", + "PREPARATION_TYPE_SWANNECK": "Swan Neck", + "PREPARATION_TYPE_DRIPPER": "Dripper", + "PREPARATION_TYPE_DELTER_PRESS": "Delter Press", + "PREPARATION_TYPE_COLD_BREW": "Koud brouwsel", + "PREPARATION_TYPE_AEROPRESS_INVERTED": "Aeropress Omgekeerd", + "PREPARATION_TYPE_TURKISH": "Turks", + "PREPARATION_TYPE_BLUE_DRIPPER": "Blue Dripper", + "PREPARATION_TYPE_ADD_CUSTOM": "Aangepaste methode toevoegen", + "PREPARATION_TYPE_GINA": "Gina", + "PREPARATION_TYPE_KONO": "Kono", + "PREPARATION_TYPE_ORIGAMI": "Origami", + "PREPARATION_TYPE_CAFELAT": "Cafelat", + "PREPARATION_TYPE_OREA": "Orea", + "PREPARATION_TYPE_COLD_DRIP": "Cold Drip", + "PREPARATION_TYPE_HAND_LEVER": "Handmatige hendel", + "PREPARATION_TYPE_FLAIR": "Flair", + "PREPARATION_TYPE_APRIL_BREWER": "April Brewer", + "PREPARATION_TYPE_ESPRO_BLOOM": "Espreo Bloom", + "PREPARATION_TYPE_FELLOW_STAGG": "Fellow Stagg", + "PREPARATION_TYPE_HSIAO_50": "Hsiao 50", + "PREPARATION_TYPE_KARLSBADER_KANNE": "Karlsbader", + "PREPARATION_TYPE_MOCCA_MASTER": "Mocca Master", + "PREPARATION_TYPE_SIPHON": "Sifon", + "CHOOSE_BEANS": "Selecteer bonen", + "CHOOSE_BEAN": "Selecteer boon", + "CHOOSE_WATERS": "Selecteer wateren", + "CHOOSE_WATER": "Selecteer water", + "CHOOSE_PREPARATIONS": "Selecteer bereidingsmethoden", + "CHOOSE_PREPARATION": "Selecteer bereidingsmethode", + "CHOOSE_MILLS": "Selecteer molens", + "CHOOSE_MILL": "Selecteer molen", + "BEAN": { + "PLACE_HOLDER": { + "BEAN_DATA_NAME": "Naam toevoegen", + "BEAN_DATA_ROAST_NAME": "Voeg de mate van branding toe", + "BEAN_DATA_ROASTING_DATE": "Wanneer werden de bonen geroosterd?", + "BEAN_DATA_ROASTER": "Wie heeft de bonen geroosterd?", + "BEAN_DATA_VARIETY": "Voeg de koffievariant toe", + "BEAN_DATA_PROCESSING": "Koffieverwerking, bijvoorbeeld gewassen", + "BEAN_DATA_COUNTRY": "Waar komt de boon vandaan?", + "BEAN_DATA_MIX": "Wat is de melange ratio?", + "BEAN_DATA_AROMATICS": "Voeg smaken toe", + "BEAN_DATA_WEIGHT": "Gewicht van de bonen", + "BEAN_DATA_COST": "Hoeveel hebben de bonen gekost?", + "BEAN_DATA_REGION": "Regio toevoegen", + "BEAN_DATA_FARM": "Voeg de boerderij toe", + "BEAN_DATA_FARMER": "Voeg de boer toe", + "BEAN_DATA_ELEVATION": "Op welke hoogte werden de bonen geteeld", + "BEAN_DATA_HARVEST_TIME": "wanneer zijn de bonen geoogst?", + "BEAN_DATA_BUY_DATE": "Wanneer zijn de bonen gekocht?", + "BEAN_DATA_PERCENTAGE": "Voeg het percentage van deze boon in de melange toe", + "BEAN_DATA_CERTIFICATION": "Voeg de bonencertificering toe (bijv. fairtrade, bio)", + "BEAN_DATA_ROASTING_TYPE": "Voeg brand type toe", + "BEAN_DATA_DECAFFEINATED": "Is deze koffie cafeïnevrij", + "BEAN_DATA_URL": "Voeg de website-url toe", + "BEAN_DATA_EAN": "Voeg het EAN- of artikelnummer toe", + "BEAN_DATA_CUPPING_POINTS": "Voeg de cuppingpunten toe", + "BEAN_DATA_PURCHASING_PRICE": "Voeg de aankoopprijs toe", + "BEAN_DATA_FOB_PRICE": "Voeg de FOB-prijs toe", + "NOTES": "Voeg notities toe over deze bonen", + "CHOOSE_DATA_ROASTER": "Kies een brander", + "CHOOSE_DATA_ROASTING_TYPE": "Kies type branding", + "BEAN_DATA_BEST_DATE": "Wanneer is de beste datum om de bonen te gebruiken?", + "BEAN_DATA_OPEN_DATE": "Wanneer is de zak bonen geopend?", + "FROZEN_NOTES": "Zijn er opmerkingen over de bevroren koffie? Bijvoorbeeld in welke vriezer je het hebt bewaard." + } + }, + "PREPARATION": { + "PLACE_HOLDER": { + "PREPARATION_TYPE_NAME": "Voeg een naam toe", + "NOTES": "Voeg notities toe over deze bereidingsmethode" + } + }, + "MILL": { + "PLACE_HOLDER": { + "NAME": "Voeg een naam toe", + "NOTES": "Voeg notities toe over deze grinder" + } + }, + "BREW": { + "PLACE_HOLDER": { + "BREW_DATA_GRIND_SIZE": "Voer de maalinstelling in", + "BREW_DATA_GRIND_WEIGHT": "Vul de hoeveelheid koffie in (gr)", + "BREW_DATA_BREW_TEMPERATURE": "Voer de brouwtemperatuur in", + "BREW_DATA_PREPARATION_METHOD": "Selecteer bereidingsmethode", + "BREW_DATA_BEAN_TYPE": "Selecteer bonen", + "BREW_DATA_MILL": "Selecteer een molen", + "BREW_DATA_MILL_SPEED": "Voer de maalsnelheid in", + "BREW_DATA_MILL_TIMER": "Voer de maal tijd in", + "BREW_DATA_PRESSURE_PROFILE": "Druk\/stroomprofiel, brouwmethodologie", + "BREW_DATA_TEMPERATURE_TIME": "Voer de tijd in die de machine heeft mogen opwarmen", + "BREW_DATA_COFFEE_BLOOMING_TIME": "Hoe lang is de bloom?", + "BREW_DATA_COFFEE_FIRST_DRIP_TIME": "Wanneer verscheen de eerste druppel?", + "BREW_DATA_BREW_QUANTITY": "Hoeveel water heb je gebruikt om te brouwen?", + "BREW_DATA_COFFEE_TYPE": "Voer de koffiestijl in (bijv. ristretto)", + "BREW_DATA_COFFEE_CONCENTRATION": "Voer de koffie concentratie in", + "BREW_DATA_TDS": "Wat was de gemeten hoeveelheid opgeloste vaste stoffen (TDS)?", + "BREW_DATA_NOTES": "Voeg notities toe over dit brouwsel", + "BREW_DATA_BREW_BEVERAGE_QUANTITY": "Voeg totale drank hoeveelheid toe", + "BREW_DATA_PREPARATION_METHOD_TOOL": "Kies je bereiding tools", + "BREW_DATA_WATER": "Kies het gebruikte water", + "BREW_DATA_BEAN_WEIGHT_IN": "Welk gewicht aan bonen heb je gebruikt?" + } + }, + "ROASTED_BEFORE": "Geroosterd voor", + "DAY_OLD": "dag oud", + "DAYS_OLD": "dagen oud", + "BEANS_AMOUNT_USED": "Verbruikt", + "CUPPING_BREW": "Proeven", + "COFFEE_DRUNKEN_QUANTITY": "Koffie dronken", + "IMAGE_DELETED": "Afbeelding is verwijderd", + "IMAGE_NOT_DELETED": "Afbeelding kon niet worden verwijderd", + "EXTERNAL_STORAGE_NOT_SUPPORTED": "Sorry, externe opslag wordt niet ondersteund", + "BEANS_ARCHIVED": "Gearchiveerd", + "TAB_ARCHIVE": "Archief", + "TODAY": "Vandaag", + "PLEASE_WAIT": "Even geduld aub...", + "PREPARATION_STYLE_POUR_OVER": "Pourover", + "PREPARATION_STYLE_ESPRESSO": "Espresso", + "PREPARATION_STYLE_FULL_IMMERSION": "Immersie", + "PREPARATION_STYLE_PERCOLATION": "Percolatie", + "PREPARATION_TYPE_STYLE": "Bereidingsstijl", + "PAGE_SETTINGS_FAST_REPEAT_DESCRIPTION": "Activeert een nieuw menu-item - hiermee kun je een brouwsel kopieren.", + "PAGE_SETTINGS_TRACK_BREW_COORDINATES_DESCRIPTION": "Sla geolocatie gegevens op voor elk brouwsel.", + "PAGE_SETTINGS_TRACK_CAFFEINE_CONSUMPTION_DESCRIPTION": "Bewaar de hoeveelheid geconsumeerde cafeïne", + "UPDATE_TITLE": "Wat is er nieuw?", + "NEXT": "Volgende", + "CUSTOM_PARAMETERS": "Parameters aanpassen", + "CUSTOM_DEFAULT_PARAMETERS": "Standaard", + "CUSTOM_MANAGE_PARAMETERS": "Beheer", + "CUSTOM_SORT_PARAMETERS": "Sorteer", + "BREW_PARAMETER_CUSTOMIZE_TITLE": "Aangepaste parameters voor elke bereidingsmethode", + "BREW_PARAMETER_CUSTOMIZE_DESCRIPTION": "Wilt u voor elke bereidingsmethode aangepaste parameters kiezen? Navigeer naar \"Methoden\", open het menu van de specifieke bereidingsmethode en kies \"Parameters aanpassen\". Daar kunt u kiezen welke parameters voor deze bereiding worden gebruikt!", + "BREW_DATA_BREW_QUANTITY_TOOLTIP": "Hoeveelheid water (niet bruikbaar voor espresso)", + "BREW_DATA_COFFEE_FIRST_DRIP_TIME_TOOLTIP": "Eerste druppel tijd van de koffie (alleen espresso)", + "BREW_DATA_PREPARATION_METHOD_TOOLTIP": "Voorbereiding (alleen aanpasbaar indien actief)", + "PAGE_SETTINGS_GENERAL": "Algemene instellingen", + "EDIT_PREPARATION_CUSTOM_PARAMETERS": "Parameters aanpassen", + "ENABLE_PREPARATION_CUSTOM_PARAMETERS": "Aangepaste parameters gebruiken", + "BEAN_ADD_ANOTHER_SORT": "Voeg een andere boon type toe", + "BEAN_SORT": "Boon Type", + "BEAN_SORT_INFORMATION": "Informatie over de variëteit", + "BEAN_SORT_MORE_INFORMATION": "Meer informatie", + "NAVIGATE_TO_PREPARATION_METHODS": "Navigeer naar bereidingsmethoden", + "PREPARATION_TOOLS": "bereidingsmiddelen", + "PREPARATION_TOOLS_INFORMATION": "Voeg verschillende mandjes toe, WDT-gereedschappen voor uw espressomachine; stoffen, papieren of gaasfilters voor uw filterkoffie, enz.", + "PREPARATION_TOOLS_PLACEHOLDER": "Papieren of stoffen filter, VST 20g, 14g mandje, enz.", + "PREPARATION_PARAMETERS_CUSTOMIZED": "Aangepaste parameters", + "BEANS_WEIGHT_AVAILABLE": "Beschikbare bonen", + "SORT_ORDER": "Sorteervolgorde wijzigen", + "ASCENDING": "Oplopend", + "DESCENDING": "Aflopend", + "SORT_AFTER": "Sorteren na", + "BEAN_SORT_NAME_OF_BEAN": "Boon naam", + "BEAN_SORT_ROASTER": "Brander", + "BEAN_SORT_ROASTING_DATE": "Datum van roosteren", + "BEAN_TAB_ROAST_INFORMATION": "Geroosterde informatie", + "BEAN_TAB_GENERAL_INFORMATION": "Algemeen", + "BEAN_TAB_SORT_INFORMATION": "Informatie over de variëteit", + "PAGE_SETTINGS_MANAGE_ARCHIVE": "Archief beheren", + "LAST_USE": "Laatst gebruikt", + "SEARCH": "Zoeken", + "OVERVIEW": "Overzicht", + "BEAN_HEADER_ADDITIONALE_INFORMATION": "Aanvullende informatie", + "THREE_DEE_TOUCH_ACTION_BREW": "Brouw", + "THREE_DEE_TOUCH_ACTION_BEAN": "Boon", + "THREE_DEE_TOUCH_ACTION_PREPARATION": "Bereidingsmethode", + "THREE_DEE_TOUCH_ACTION_MILL": "Koffiemolen", + "PAGE_CREDITS_NOT_EXISTING": "Geen inhoud", + "TIMER_HOUR": "Uren", + "TIMER_MINUTES": "Minuten", + "TIMER_SECONDS": "Seconden", + "EXCEL": { + "BEAN": { + "CREATION_DATE": "Aanmaakdatum", + "ID": "Bonen-ID" + }, + "PREPARATION": { + "CREATION_DATE": "Aanmaakdatum", + "ID": "bereidingsmiddelen" + }, + "GRINDER": { + "CREATION_DATE": "Aanmaakdatum", + "ID": "Molen-ID" + } + }, + "EXCEL_EXPORT": "Excel-export", + "HEALTH_KIT_QUESTION_TITLE": "cafeïneconsumptie opslaan", + "HEALTH_KIT_QUESTION_MESSAGE": "Na activering wordt de geschatte cafeïne van elk brouwsel automatisch opgeslagen in Apple Health.", + "NAV_ROASTING_SECTION": "Bonen roosteren", + "ROASTING_SECTION": { + "NAV_GREEN_BEANS": "Groene bonen", + "NAV_ROASTING_MACHINE": "Roostermachine", + "ROASTING_MACHINE": { + "TOTAL_ROAST_QUANTITY": "Totaalgewicht geroosterd", + "TOTAL_ROAST_COUNT": "Aantal brandingen" + }, + "GREEN_BEAN": { + "ADD": "Toevoegen", + "EDIT": "Bewerking", + "DETAIL": "Groene bonen details", + "ROASTABLE": "Roosterbaar", + "NO_ROASTS_YET": "Nog geen gebrande bonen" + }, + "BEAN": { + "DROP_TEMPERATURE": "Eindtemperatuur van de gebrande boon", + "ROAST_LENGTH": "brand lengte", + "ROASTER_MACHINE": "Roostermachine", + "GREEN_BEAN_WEIGHT": "Gewicht van groene bonen", + "OUTSIDE_TEMPERATURE": "Omgevingstemperatuur", + "HUMIDITY": "Vochtigheid", + "FIRST_CRACK_MINUTE": "eerst kraak minuut", + "FIRST_CRACK_TEMPERATURE": "eerste kraak temperatuur", + "SECOND_CRACK_MINUTE": "Tweede kraak minuut", + "SECOND_CRACK_TEMPERATURE": "tweede kraak temperatuur", + "PLACE_HOLDER": { + "DROP_TEMPERATURE": "Eindtemperatuur van de gebrande boon", + "ROAST_LENGTH": "brand lengte", + "ROASTER_MACHINE": "Roostermachine", + "GREEN_BEAN_WEIGHT": "Gewicht van groene bonen", + "OUTSIDE_TEMPERATURE": "Omgevingstemperatuur", + "HUMIDITY": "Vochtigheid", + "FIRST_CRACK_MINUTE": "Eerste kraak minuut", + "FIRST_CRACK_TEMPERATURE": "Eerste kraak temperatuur", + "SECOND_CRACK_TEMPERATURE": "Tweede kraak temperatuur", + "SECOND_CRACK_MINUTE": "Tweede kraak minuut" + } + } + }, + "PAGE_SETTINGS_MANAGE_SECTIONS": "Meer secties", + "PAGE_SETTINGS_SHOW_ROASTING_SECTION": "Activeer brandersectie", + "PAGE_SETTINGS_SHOW_WATER_SECTION": "Activeer water sectie", + "PAGE_SETTINGS_SHOW_CUPPING_SECTION": "Activeer cupping-sectie", + "BEAN_DATA_BUY_DATE": "Aankoopdatum", + "BEAN_SORT_CREATION_DATE": "Aanmaakdatum", + "BEAN_SORT_PURCHASE_DATE": "Aankoopdatum", + "BEAN_ROAST_COUNT": "Roost aantal", + "TRANSFER_ROAST": "Geroosterd", + "BEAN_TAB_LINKED_ROASTS": "Geroosterd", + "BEAN_DATA_WEIGHT_AFTER_ROASTING": "Gewicht na het roosteren", + "TOAST_GREEN_BEAN_ADDED_SUCCESSFULLY": "Groene bonen toegevoegd", + "TOAST_GREEN_BEAN_EDITED_SUCCESSFULLY": "Groene boon bewerkt", + "TOAST_GREEN_BEAN_ARCHIVED_SUCCESSFULLY": "Groene bonen gearchiveerd", + "TOAST_ROASTING_MACHINE_ADDED_SUCCESSFULLY": "Roostermachine toegevoegd", + "TOAST_ROASTING_MACHINE_EDITED_SUCCESSFULLY": "Brandmachine bewerkt", + "TOAST_ROASTING_MACHINE_ARCHIVED_SUCCESSFULLY": "Brander gearchiveerd", + "DELETE_ROASTING_MACHINE_QUESTION": "Brander verwijderen? Alle gebrande bonen waarnaar verwezen wordt, worden bijgewerkt en niet verwijderd.", + "TOAST_ROASTING_MACHINE_DELETED_SUCCESSFULLY": "Brander verwijderd", + "EDIT_ROASTING_MACHINE": "Bewerk", + "DETAIL_ROASTING_MACHINE": "Details van de brander", + "DELETE_WATER_QUESTION": "Water verwijderen? Alle gerefereerde brouwsels worden bijgewerkt en niet verwijderd", + "ROASTING_MACHINE": { + "PLACE_HOLDER": { + "NAME": "Voeg een naam toe", + "NOTES": "Opmerkingen toevoegen voor deze brander" + } + }, + "NAV_ROASTING_MACHINE": "koffierooster machines", + "PAGE_ROASTING_MACHINE_LIST_NO_MACHINES_EXISTING": "U hebt geen koffiebrander toegevoegd", + "PAGE_ROASTING_MACHINE_LIST_NO_ARCHIVED_MACHINES_EXISTING": "je hebt geen koffiebrander gearchiveerd", + "CHOOSE_ROASTING_MACHINES": "koffiebrander", + "CHOOSE_ROASTING_MACHINE": "koffiebrander", + "POPOVER_BREWS_OPTION_TOGGLE_FAVOURITE": "Favoriet", + "TOAST_BREW_FAVOURITE_ADDED": "Favoriet toegevoegd", + "TOAST_BREW_FAVOURITE_REMOVED": "Favoriet verwijderd", + "BREW_FILTER_JUST_FAVOURITE": "Favorieten", + "STATISTICS_PREPARATION_USAGES": "Bereidingsmethode gebruik", + "STATISTICS_PREPARATION_TIMELINE_USAGES": "Gebruiksgeschiedenis bereidingsmethode", + "STATISTICS_GRINDER_TIMELINE_USAGES": "Gebruiksgeschiedenis van de Maler", + "ACCEPT": "Accepteer", + "STATISTIC_TAB_GENERAL": "Algemeen", + "STATISTIC_TAB_BREWS": "Brouwsels", + "STATISTIC_TAB_BEANS": "Bonen", + "STATISTIC_TAB_PREPARATIONS": "Voorbereidingen", + "STATISTIC_TAB_GRINDERS": "Malers", + "PAGE_STATISTICS_BREW_PER_DAYPROCESSES": "Brouwsels per dag", + "PAGE_STATISTICS_BREW_TIME": "Brouwtijd", + "PAGE_STATISTICS_PHOTOS_TAKEN": "Foto's gemaakt", + "PAGE_SETTINGS_IMAGE_QUALITY": "afbeelding kwaliteit", + "PAGE_SETTINGS_IMAGE_QUALITY_TOOLTIP": "Bepaal in welke kwaliteit uw afbeeldingen moeten worden opgeslagen. Dit kan uw dataverbruik verlagen.", + "PAGE_SETTINGS_BREW_RATING": "Brouw beoordeling", + "PAGE_SETTINGS_BREW_RATING_TOOLTIP": "Is de standaard '-1 tot 5' niet de juiste beoordeling voor u? U kunt in plaats daarvan een '-1 tot 100' schaal gebruiken", + "UPDATE_ENTRY_OF": "Update item {{index}} van {{count}}", + "WEBSITE": "Website", + "SHARE": "Deel", + "ANDROID_FILE_ACCESS_NEEDED_TITLE": "Toegang tot gedeeld bestand vereist", + "ANDROID_FILE_ACCESS_NEEDED_DESCRIPTION": "Om de app volledig te laten werken, vragen we u om bestandstoegang te autoriseren. Anders ontstaan er problemen bij het gebruik van de app. Dit is specifiek nodig voor het automatische back-upsysteem.", + "COULD_NOT_ACCESS_FILE": "We konden het gekozen bestand niet openen", + "WRONG_FILE_FORMAT": "U hebt een niet-ondersteund bestandsformaat gekozen", + "SCAN_BEAN": "Scan pakket", + "CLEAR": "leeg maken", + "BEAN_LOOKS_LIKE_CONSUMED": "Het lijkt erop dat deze bonen op zijn. Wil je ze archiveren?", + "CUPPING_1": "Fruit", + "CUPPING_2": "Citrus", + "CUPPING_3": "Citroen & limonade", + "CUPPING_4": "Limoen", + "CUPPING_5": "Grapefruit", + "CUPPING_6": "Clementine", + "CUPPING_7": "Mandarijn", + "CUPPING_8": "mandarijn sinaasappel", + "CUPPING_9": "sinasappel", + "CUPPING_10": "Appel\/peer", + "CUPPING_11": "Groene appel", + "CUPPING_12": "Rode appel", + "CUPPING_13": "Meloen", + "CUPPING_14": "Watermeloen", + "CUPPING_15": "Honingdauw meloen", + "CUPPING_16": "Cantaloupe meloen", + "CUPPING_17": "Druif", + "CUPPING_18": "Witte druif", + "CUPPING_19": "Groene druif", + "CUPPING_20": "Rode druif", + "CUPPING_21": "Concord-druif", + "CUPPING_22": "Tropisch fruit", + "CUPPING_23": "Litchi", + "CUPPING_24": "Stervrucht", + "CUPPING_25": "Tamarinde", + "CUPPING_26": "Passievrucht", + "CUPPING_27": "Ananas", + "CUPPING_28": "Mango", + "CUPPING_29": "Papaja", + "CUPPING_30": "Kiwi", + "CUPPING_31": "Banaan", + "CUPPING_32": "Kokosnoot", + "CUPPING_33": "Steenvrucht", + "CUPPING_34": "Perzik", + "CUPPING_35": "Nectarine", + "CUPPING_36": "Abrikoos", + "CUPPING_37": "Pruim", + "CUPPING_38": "Kers", + "CUPPING_39": "Zwarte kers", + "CUPPING_40": "Bes", + "CUPPING_41": "Cranberry", + "CUPPING_42": "Framboos", + "CUPPING_43": "Aardbei", + "CUPPING_44": "Bosbes", + "CUPPING_45": "Rode bes", + "CUPPING_46": "Zwarte bes", + "CUPPING_47": "Gedroogd fruit", + "CUPPING_48": "Gouden rozijn", + "CUPPING_49": "Rozijn", + "CUPPING_50": "Gedroogde vijg", + "CUPPING_51": "Gedroogde dadels", + "CUPPING_52": "Gedroogde Pruim", + "CUPPING_53": "Zoet & gebrand", + "CUPPING_54": "Chocolade", + "CUPPING_55": "Cacaonibs", + "CUPPING_56": "Donkere chocolade", + "CUPPING_57": "Bakkers chocolade", + "CUPPING_58": "Bitterzoete chocolade", + "CUPPING_59": "Cacaopoeder", + "CUPPING_60": "Melkchocolade", + "CUPPING_61": "Noot", + "CUPPING_62": "Walnoot", + "CUPPING_63": "Pinda", + "CUPPING_64": "Cashew", + "CUPPING_65": "Pecannoot", + "CUPPING_66": "Hazelnoot", + "CUPPING_67": "Amandel", + "CUPPING_68": "Graan & Graanproducten", + "CUPPING_69": "Zoet broodgebak", + "CUPPING_70": "Granola", + "CUPPING_71": "Graham-cracker", + "CUPPING_72": "Rogge", + "CUPPING_73": "Tarwe", + "CUPPING_74": "Gerst", + "CUPPING_75": "Vers brood", + "CUPPING_76": "Zoet & Suikerachtig", + "CUPPING_77": "Vanille", + "CUPPING_78": "Noga", + "CUPPING_79": "Honing", + "CUPPING_80": "Boter", + "CUPPING_81": "Room", + "CUPPING_82": "Marshmallow", + "CUPPING_83": "Rietsuiker", + "CUPPING_84": "Bruine suiker", + "CUPPING_85": "Karamel", + "CUPPING_86": "Ahornsiroop", + "CUPPING_87": "Melasse", + "CUPPING_88": "Cola", + "CUPPING_89": "Geroosterd", + "CUPPING_90": "Toast", + "CUPPING_91": "Verbrande suiker", + "CUPPING_92": "Rokerig", + "CUPPING_93": "Koolstof", + "CUPPING_94": "Plantaardig, hartig en kruidig", + "CUPPING_95": "Kruiden", + "CUPPING_96": "Zwarte peper", + "CUPPING_97": "Witte peper", + "CUPPING_98": "Kaneel", + "CUPPING_99": "Koriander", + "CUPPING_100": "Gember", + "CUPPING_101": "Nootmuskaat", + "CUPPING_102": "Kerrie", + "CUPPING_103": "Drop-anijs", + "CUPPING_104": "Kruidnagel", + "CUPPING_105": "Hartig", + "CUPPING_106": "Leerachtig", + "CUPPING_107": "Vleesachtig", + "CUPPING_108": "Sojasaus", + "CUPPING_109": "Zongedroogde tomaat", + "CUPPING_110": "Tomaat", + "CUPPING_111": "Plantaardig aards kruid", + "CUPPING_112": "Grond", + "CUPPING_113": "Vers hout", + "CUPPING_114": "Ceder", + "CUPPING_115": "Tabak", + "CUPPING_116": "Hooi \/ stro", + "CUPPING_117": "Bladgroenten", + "CUPPING_118": "Olijf", + "CUPPING_119": "Groene peper", + "CUPPING_120": "Pompoen", + "CUPPING_121": "Paddestoel", + "CUPPING_122": "Zoete erwt", + "CUPPING_123": "Sneeuwerwt", + "CUPPING_124": "Grassig", + "CUPPING_125": "Dille", + "CUPPING_126": "Salie", + "CUPPING_127": "Munt", + "CUPPING_128": "Groene thee", + "CUPPING_129": "Zwarte thee", + "CUPPING_130": "Hop", + "CUPPING_131": "Bergamot", + "CUPPING_132": "Bloemrijk", + "CUPPING_133": "Bloemen", + "CUPPING_134": "Hibiscus", + "CUPPING_135": "Rozenbottels", + "CUPPING_136": "Lavendel", + "CUPPING_137": "Magnolia", + "CUPPING_138": "Jasmijn kamperfoelie", + "CUPPING_139": "Oranjebloesem", + "CUPPING_140": "Citroengras", + "WATER_SECTION": { + "NAV_WATER": "Water", + "YOU_GOT_NO_ARCHIVED_WATER": "Je hebt nog geen water gearchiveerd", + "YOU_GOT_NO_WATER": "Je hebt nog geen water toegevoegd", + "CATEGORY_INFORMATION": "Water informatie", + "CATEGORY_GENERAL": "Algemeen", + "WATER_BOTTLE_EXPLANATION": "Waterflessen vermelden de concentratie meestal in eenheden van ppm = mg\/L", + "USED_TIMES": "Aantal keren gebruikt", + "AMOUNT": "Gebruikte hoeveelheid", + "WATER": { + "GENERAL_HARDNESS": "Algemene hardheid (GH)", + "TOTAL_ALKALINITY": "Totale alkaliteit (KH)", + "CALCIUM": "Kalium (Ca)", + "MAGNESIUM": "Magnesium (Mg)", + "SODIUM": "Natrium (Na)", + "TDS": "Totaal opgeloste vaste stoffen (TDS)", + "UNITS": "Eenheden", + "PLACE_HOLDER": { + "GENERAL_HARDNESS": "Algemene hardheid", + "TOTAL_ALKALINITY": "Totale alkaliteit", + "CALCIUM": "Kalium (Ca)", + "MAGNESIUM": "Magnesium (Mg)", + "SODIUM": "Natrium (Na)", + "TDS": "Totaal opgeloste vaste stoffen (TDS)", + "NAME": "Voeg waternaam toe", + "NOTES": "Voeg wat notities toe voor je water", + "POTASSIUM": "Kalium (K)", + "CHLORIDE": "Chloride (Cl)", + "SULFATE": "Sulfaat (SO4)" + }, + "WATER_UNIT": { + "UNKNOWN": "Onbekend", + "PPM": "ppm als CaCO3", + "MG_L": "mg\/L", + "MMOL_L": "mmol\/L", + "DH": "°dH" + }, + "POTASSIUM": "Kalium (K)", + "CHLORIDE": "Chloride (Cl)", + "SULFATE": "Sulfaat (SO4)" + } + }, + "BREW_BRIX_CALCULATION": "Graden Brix", + "SET_TDS": "TDS instellen", + "TOTAL_WEIGHT": "Totaal gewicht", + "CALCULATED_WEIGHT": "Berekend gewicht", + "SET_WEIGHT": "Gewicht instellen", + "ADD_FLAVORS_AROMAS_TITLE": "Aroma's \/ Smaken", + "CUSTOM_FLAVORS_AROMAS": "Individueel aroma", + "CUSTOM_FLAVORS_AROMAS_PLACEHOLDER": "Voeg je individuele aroma toe", + "PREDEFINED_FLAVORS_AROMAS": "Veel voorkomende aroma's", + "ADD_AROMA_FLAVOR": "Aroma's\/smaken toevoegen", + "BEAN_WEIGHT_IN_PLACEHOLDER": "Bonen uit de verpakking gehaald", + "VESSEL_PLACEHOLDER": "Naam van het Serveerkan en het leeggewicht ervan", + "GRIND_WEIGHT_PLACEHOLDER": "Gewicht van de gemalen bonen die voor het brouwen worden gebruikt", + "PRESET_BREW_TITLE": "Gebruik laatste brouwsel als voorinstelling", + "CUPPING_BREW_TAB_AROMA": "Aroma", + "CUPPING_BREW_TAB_TASTING": "Systematische cupping", + "WATER_PLACEHOLDER": "Activeer het watergedeelte in het instellingenmenu voor volledige functionaliteit", + "PAGE_SETTINGS_SCALES": "Weegschalen", + "CONNECT": "Verbinden", + "DISCONNECT": "Loskoppelen", + "SCALE": { + "BLUETOOTH_SCAN_RUNNING": "Zoeken naar weegschaal tot 60s", + "BLUETOOTH_NOT_ENABLED": "Bluetooth niet geactiveerd, activeer het om het goed te laten werken", + "CONNECTION_NOT_ESTABLISHED": "Weegschaal niet gevonden, of verbinding kon niet tot stand worden gebracht", + "CONNECTED_SUCCESSFULLY": "Weegschaal verbonden", + "DISCONNECTED_SUCCESSFULLY": "Weegschaal losgekoppeld", + "DISCONNECTED_UNPLANNED": "Weegschaal onverwachts losgekoppeld", + "REQUEST_PERMISSION": { + "LOCATION": "Om Bluetooth-weegschalen te vinden, heeft de app toegang nodig tot de locatie.", + "BLUETOOTH": "Om bluetooth-weegschalen te vinden, heeft de app toegang tot bluetooth nodig" + }, + "INFORMATION_DESCRIPTION": "Ondersteunde weegschalen zijn: Decent Scale, Acaia, Felicita, Hiroia Jimmy, Skale 2, DiFluid Microbalance, Smartchef Scale, Blackcoffee.io, Bookoo Mini Scale en Eureka Precisa. Let op: Als de Eureka Precisa een negatieve waarde ontvangt, stopt de timer" + }, + "QR": { + "WRONG_QRCODE_DESCRIPTION": "Ongeldige QR-code of onbekende inhoud", + "WRONG_QRCODE_TITLE": "Fout", + "WRONG_LINK_DESCRIPTION": "Ongeldige QR-link", + "WRONG_LINK_TITLE": "Fout", + "SERVER": { + "ERROR_OCCURED": "Er is een fout opgetreden. De QR-code kon niet worden gelezen. Probeer het opnieuw.", + "BEAN_NOT_APPROVED": "Boon is nog niet goedgekeurd, probeer het later nog eens" + }, + "BEAN_SUCCESSFULLY_SCANNED": "Bean succesvol gescand", + "BEAN_SUCCESSFULLY_REFRESHED": "Boon succesvol bijgewerkt", + "IMAGES_GETTING_DOWNLOADED": "Afbeeldingen downloaden" + }, + "RETRY_CONNECT": "Verbinding opnieuw proberen", + "SMART_SCALE_STAY_CONNECTED_ON_APP_MINIMIZE": "Houd de weegschaal verbonden, zelfs als de app op de achtergrond draait", + "BREW_FLOW_WEIGHT": "Gewicht", + "BREW_FLOW_WEIGHT_PER_SECOND": "Stroom (afgevlakt)", + "ROAST_TYPE_UNKNOWN": "onbekend", + "ROAST_TYPE_CINNAMON_ROAST": "Cinnamon Branding", + "ROAST_TYPE_AMERICAN_ROAST": "Amerikaans Branding", + "ROAST_TYPE_NEW_ENGLAND_ROAST": "Nieuw-Engeland Branding", + "ROAST_TYPE_HALF_CITY_ROAST": "Half City Branding", + "ROAST_TYPE_MODERATE_LIGHT_ROAST": "Matig-lichte branding", + "ROAST_TYPE_CITY_ROAST": "City Branding", + "ROAST_TYPE_CITY_PLUS_ROAST": "City+ Branding", + "ROAST_TYPE_FULL_CITY_ROAST": "Full City Branding", + "ROAST_TYPE_FULL_CITY_PLUS_ROAST": "Full City + Branding", + "ROAST_TYPE_ITALIAN_ROAST": "Ialiaanse Branding", + "ROAST_TYPE_VIEANNA_ROAST": "Weense Branding", + "ROAST_TYPE_FRENCH_ROAST": "Franse Branding", + "ROAST_TYPE_CUSTOM_ROAST": "Aangepast", + "BEAN_MIX_UNKNOWN": "onbekend", + "BEAN_MIX_SINGLE_ORIGIN": "Single Origin", + "BEAN_MIX_BLEND": "Melange", + "BEAN_ROASTING_TYPE_FILTER": "Filter", + "BEAN_ROASTING_TYPE_ESPRESSO": "Espresso", + "BEAN_ROASTING_TYPE_OMNI": "Omni", + "BEAN_ROASTING_TYPE_UNKNOWN": "Onbekend", + "SMART_SCALE_LOG": "Activeer logbestanden voor slimme weegschaal (alleen voor foutopsporing)", + "TOAST_PREPARATION_TOOL_EDITED_SUCCESSFULLY": "Voorbereidingstool bewerkt", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE": "Bluetooth-weegschaal", + "PAGE_SETTINGS_TAB_GENERAL": "Algemeen", + "SMART_SCALE_TARE_ON_BREW": "Tarraweegschaal op nieuw brouwsel", + "SMART_SCALE_TARE_ON_START_TIMER": "Tarra weegschaal bij het starten van de timer", + "PAGE_SETTINGS_BREW_RATING_STEPS": "Beoordelingsstappen", + "BREW_AVG_FLOW_WEIGHT_PER_SECOND": "Ø Stroom", + "CUSTOM_LIST_VIEW_PARAMETERS": "Parameters voor lijstweergave", + "NAV_LIST_VIEW_CUSTOM_PARAMETERS": "Parameters voor lijstweergave", + "PAGE_LIST_VIEW_CUSTOM_PARAMETERS_DESCRIPTION": "Bepaal welke parameters moeten worden weergegeven op de tegels van de lijstweergave", + "BREW_DATA_VESSEL_NAME_WEIGHT": "Serveerkan Naam\/Gewicht", + "IGNORE_NEGATIVE_VALUES": "Negeer negatieve gewichtswaarden", + "IGNORE_ANOMALY_VALUES": "Negeer afwijkende waarden", + "IGNORE_ANOMALY_VALUES_TOOLTIP": "Bijvoorbeeld: Een kopje rond bewegen op de weegschaal", + "TOAST_BEAN_FAVOURITE_ADDED": "Favoriet toegevoegd", + "TOAST_BEAN_FAVOURITE_REMOVED": "Favoriet verwijderd", + "QR_CODE_SCANNER_INFORMATION_TITLE": "QR-code", + "QR_CODE_SCANNER_INFORMATION_DESCRIPTION": "Alle gescande boneninformatie komt rechtstreeks van de branderij. Mocht u onjuiste of misleidende informatie aantreffen, laat het mij dan weten via e-mail: info@beanconqueror.com.", + "DONT_SHOW_AGAIN": "Niet meer weergeven", + "ARCHIVED_TOOLS": "Gearchiveerd gereedschap", + "UNARCHIVE": "Herstel", + "PAGE_SETTINGS_HIDE_ARCHIVED_BREWS_DASHBOARD": "Toon gearchiveerde brouwsels op dashboard", + "PAGE_SETTINGS_HIDE_ARCHIVED_BREWS_DASHBOARD_DESCRIPTION": "Moeten gearchiveerde brouwsels op de startpagina worden weergegeven?", + "COPY": "Kopiëren", + "TOAST_PREPARATION_METHOD_REPEATED_SUCCESSFULLY": "Bereidingsmethode succesvol gekopieerd", + "PREPARATION_TYPE_CAFEC_FLOWER": "Cafec Flower", + "PREPARATION_TYPE_DECEMBER_DRIPPER": "December Dripper", + "PREPARATION_TYPE_DECENT_ESPRESSO": "Decent Espresso", + "PREPARATION_TYPE_HARIO_SWITCH": "Hario Switch", + "PREPARATION_TYPE_HARIO_WOODNECK": "Hario Woodneck", + "PREPARATION_TYPE_RATIO_SIX_COFFEE_BREWER": "Ratio Six-koffiezetapparaat", + "PREPARATION_TYPE_ROK": "ROK", + "PREPARATION_TYPE_TORNADO_DUO": "Tornado-duo", + "PREPARATION_TYPE_TRICOLATE": "Tricolate", + "QR_CODE_REFRESH_DATA_MESSAGE": "Alle informatie voor deze boon wordt overschreven, doorgaan?", + "POPOVER_QR_CODE_REFRESH": "Gegevens opnieuw laden", + "BREW_FLOW_WEIGHT_REALTIME": "Stroom (realtime)", + "SMART_SCALE_STOP_TIMER_ON_BREW": "Stop de timer van de weegschaal bij een nieuw brouwsel", + "SMART_SCALE_RESET_TIMER_ON_BREW": "Reset de weegschaaltimer bij een nieuw brouwsel", + "BREW_PRESSURE_FLOW": "Druk", + "BREW_TEMPERATURE_REALTIME": "Temperatuur", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_SHOW_GRAPHS_FILTER": "Grafieken weergeven voor filter", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_SHOW_GRAPHS_ESPRESSO": "Toon grafieken voor espresso", + "PAGE_SETTINGS_TAB_BLUETOOTH_PRESSURE": "Drukapparaat", + "PAGE_SETTINGS_TAB_BLUETOOTH_TEMPERATURE": "Temperatuur apparaat", + "PRESSURE_LOG": "Activeer logbestanden voor drukapparaat", + "PRESSURE_THRESHOLD_ACTIVE": "Starttimer met vooraf gedefinieerde druk", + "PRESSURE_THRESHOLD_BAR": "Drempelwaarde druk", + "TIMER_MILLISECONDS": "MS", + "PAGE_SETTINGS_BREW_ENABLE_MILLISECONDS": "Milliseconden?", + "PAGE_SETTINGS_BREW_ENABLE_MILLISECONDS_DESCRIPTION": "Gebruik milliseconden om uw brouwsels nog nauwkeuriger te analyseren", + "PAGE_SETTINGS_BEAN_RATING": "Bonen beoordeling", + "PAGE_SETTINGS_BEAN_RATING_TOOLTIP": "Is de standaard '0 tot 5' niet de juiste beoordeling voor u? U kunt in plaats daarvan een '0 tot 100'-schaal gebruiken", + "PAGE_SETTINGS_BEAN_RATING_STEPS": "Stappen voor het beoordelen van bonen", + "COFFEE_GRAMS_BREWED": "grammen gebrouwen", + "SMART_SCALE_MAXIMIZE_ON_START_TIMER": "Maximaliseer realtime-grafiek bij het starten van de timer", + "PRESSURE": { + "CONNECTION_NOT_ESTABLISHED": "Drukapparaat niet gevonden, of verbinding kon niet tot stand worden gebracht", + "CONNECTED_SUCCESSFULLY": "Drukapparaat aangesloten", + "DISCONNECTED_SUCCESSFULLY": "Drukapparaat losgekoppeld", + "DISCONNECTED_UNPLANNED": "Drukapparaat onverwachts losgekoppeld", + "STAY_CONNECTED_ON_APP_MINIMIZE": "Houd het drukapparaat aangesloten, zelfs als de app op de achtergrond draait", + "INFORMATION_DESCRIPTION": "Ondersteunde apparaten zijn: Popsicle, Pressensor, Smart Espresso Profiler, Bookoo Espresso Monitor", + "BLUETOOTH_SCAN_RUNNING": "Zoeken naar drukapparaat gedurende maximaal 60 seconden", + "BLUETOOTH_NOT_ENABLED": "Bluetooth niet geactiveerd, activeer het om het goed te laten werken", + "REQUEST_PERMISSION": { + "LOCATION": "Om drukapparaten te vinden, heeft de app toegang nodig tot de locatie.", + "BLUETOOTH": "Om drukapparaten te vinden, heeft de app toegang tot Bluetooth nodig" + } + }, + "POPOVER_BLUETOOTH_ACTION_RECONNECT_SCALE": "Weegschaal opnieuw verbinden", + "POPOVER_BLUETOOTH_ACTION_RECONNECT_PRESSURE_DEVICE": "Drukapparaat opnieuw aansluiten", + "POPOVER_SHOW_BREWS": "Toon brouwsels", + "LAST_USED_GRIND_SIZE_SETTING": "Laatste maalstand", + "LAST_USED_BEAN": "Laatste boon", + "PAGE_SETTINGS_BREW_MILLISECONDS_DECIMAL_PLACES_DESCRIPTION": "Hoeveel decimalen moeten er worden weergegeven?", + "SMART_SCALE_COMMAND_DELAY": "Opdracht vertraging", + "SMART_SCALE_COMMAND_DELAY_TOOLTIP": "Hoeveel tijd moet er verstrijken tussen elk bluetooth-commando?", + "SUPPORT_ME": "Donatie", + "PAGE_SETTINGS_BREW_DISPLAY_BEAN_IMAGE": "Bonenafbeeldingen op brouwsels weergeven?", + "PAGE_SETTINGS_BREW_DISPLAY_BEAN_IMAGE_DESCRIPTION": "Als er een bonenafbeelding bestaat, wordt deze gebruikt in plaats van de afbeelding van de bereidingsmethode", + "DOWNLOAD_XLSX": "Excel downloaden", + "DOWNLOAD_JSON": "JSON downloaden", + "NAV_BEAN_PARAMS": "Boon Parameters", + "BEAN_DATA_ROAST_NAME_TYPE": "Brand graad", + "ENABLE_BEAN_SORT_INFORMATION": "Variëteit-informatie inschakelen", + "BEAN_SORT_MIX": "Bonenmix", + "PAGE_MANAGE_BEAN_PARAMETERS_DESCRIPTION": "Markeer welke parameters moeten worden weergegeven bij het bewerken van bean-informatie.", + "PAGE_BEAN_LIST_VIEW_CUSTOM_PARAMETERS_DESCRIPTION": "Bepaal de parameters die moeten worden weergegeven in lijstweergaven voor bonen", + "BEAN_DATA_NAME_TOOLTIP": "De naam van de bonen", + "BEAN_DATA_ROASTER_TOOLTIP": "Wie is de brander", + "BEAN_DATA_BUY_DATE_TOOLTIP": "Wanneer heb je de bonen gekocht?", + "BEAN_DATA_ROASTING_DATE_TOOLTIP": "Wat is de branddatum van de bonen?", + "BEAN_DATA_ROASTING_TYPE_TOOLTIP": "Voor welke bereidingswijze is deze boon geroosterd, bijvoorbeeld Filter", + "BEAN_DATA_ROAST_NAME_TOOLTIP": "Wat is de brandingsgraad van de bonen?", + "BEAN_DATA_ROAST_NAME_TYPE_TOOLTIP": "Welk type branding (Cinnamon, New England, etc.)", + "BREW_DATA_RATING_TOOLTIP": "Je waardering", + "BEAN_SORT_MIX_TOOLTIP": "Zijn de bonen een melange?", + "BEAN_DATA_WEIGHT_TOOLTIP": "Hoeveel wegen de bonen?", + "BEAN_DATA_COST_TOOLTIP": "Hoeveel hebben de bonen gekost?", + "BEAN_DATA_AROMATICS_TOOLTIP": "Welke aroma's\/smaken hebben de bonen?", + "BEAN_DATA_CUPPING_POINTS_TOOLTIP": "Hoeveel cupping-punten heeft de boon?", + "BEAN_DATA_DECAFFEINATED_TOOLTIP": "Is het cafeïnevrij?", + "BEAN_DATA_URL_TOOLTIP": "URL naar de winkel", + "BEAN_DATA_EAN_TOOLTIP": "Het EAN\/artikelnummer", + "NOTES_TOOLTIP": "Aantekeningen gemaakt", + "BREW_DATA_ATTACHMENTS_TOOLTIP": "Foto's toevoegen?", + "ENABLE_BEAN_SORT_INFORMATION_TOOLTIP": "Meer informatie over de bonen: waar komen ze vandaan, wanneer zijn ze geoogst, etc.", + "BEAN_DATA_COUNTRY_TOOLTIP": "Uit welk land komen de bonen?", + "BEAN_DATA_REGION_TOOLTIP": "Uit welke regio komen de bonen?", + "BEAN_DATA_FARM_TOOLTIP": "Van welke boerderij komen de bonen?", + "BEAN_DATA_FARMER_TOOLTIP": "Wie verbouwde deze bonen", + "BEAN_DATA_ELEVATION_TOOLTIP": "Op welke hoogte groeiden de bonen?", + "BEAN_DATA_PROCESSING_TOOLTIP": "Hoe zijn de bonen verwerkt (natuurlijk, gewassen, enz.)", + "BEAN_DATA_VARIETY_TOOLTIP": "De Koffieboon Variëteit (SL28 bv.)", + "BEAN_DATA_HARVEST_TIME_TOOLTIP": "In welk jaar\/maand werden deze bonen geoogst?", + "BEAN_DATA_PERCENTAGE_TOOLTIP": "Hoeveel procent van deze bonen zit er in de zak?", + "BEAN_DATA_CERTIFICATION_TOOLTIP": "Welke certificeringen hebben de bonen (bijvoorbeeld fairtrade)", + "BEAN_DATA_PURCHASING_PRICE_TOOLTIP": "Wat was de aankoopprijs van de koffiebrander", + "BEAN_DATA_FOB_PRICE_TOOLTIP": "Wat was de Free-On-Board (FOB) prijs toen de brander het kocht?", + "BEAN_PARAMETER_CUSTOMIZE_TITLE": "Pas de informatie aan die u voor bonen wilt gebruiken", + "BEAN_PARAMETER_CUSTOMIZE_DESCRIPTION": "Er kan veel informatie over de bonen worden ingevoerd of gebruikt. Kies zelf de parameters die u wilt invullen en welke u wilt weergeven", + "BREW_CANT_START_BECAUSE_TIMER_NOT_RESETTED_TITLE": "Timer resetten!", + "BREW_CANT_START_BECAUSE_TIMER_NOT_RESETTED_DESCRIPTION": "Omdat je een Bluetooth-apparaat hebt aangesloten, moet je eerst je timer resetten voordat je kunt beginnen.", + "BREW_FILTER_JUST_CHART_DATA": "Alleen grafieken", + "SCALE_RESET_TRIGGERED_DESCRIPTION": "De bluetooth weegschaal wil zowel de app-timer als de brouwgrafiek resetten, wil je doorgaan?", + "SCALE_RESET_TRIGGERED_TITLE": "Opnieuw instellen?", + "NAV_REPEAT_PARAMETERS": "Herhalingsparameters definiëren", + "PAGE_REPEAT_PARAMETERS_DESCRIPTION": "Markeer welke parameters vooraf moeten worden ingesteld wanneer u een specifieke brouwbeurt herhaalt", + "CUSTOM_REPEAT_PARAMETERS": "Herhalen", + "CUSTOM_REPEAT_PARAMETERS_DESCRIPTION": "Aangepaste herhaling activeren?", + "PAGE_SETTINGS_USE_NUERMIC_KEYBOARD_FOR_GRIND_SIZE": "Numeriek toetsenbord voor maalinstelling?", + "PAGE_SETTINGS_USE_NUERMIC_KEYBOARD_FOR_GRIND_SIZE_DESCRIPTION": "Wilt u een numeriek toetsenbord gebruiken voor de maalinstelling, in plaats van het hele toetsenbord?", + "PREPARATION_DEVICE": { + "TYPE": { + "NONE": "Geen", + "XENIA": "Xenia", + "METICULOUS": "Meticulous" + }, + "URL": "URL-adres", + "CHOOSE_DEVICE": "Kies apparaat", + "CONNECTION": { + "UNSUCCESFULLY": "Er kon geen verbinding met de machine worden gemaakt", + "SUCCESFULLY": "Verbinding met machine was succesvol" + }, + "TYPE_XENIA": { + "TITLE": "Xenia machine", + "PRESS_START_SCRIPT": "Start script bij start", + "FIRST_DRIP_SCRIPT": "Start script bij eerste druppel", + "SCRIPT_AT_WEIGHT": "Gewicht voor scriptuitvoering", + "SCRIPT_LIST_GENERAL_0": "Niets", + "SCRIPT_LIST_GENERAL_1": "Espresso, 25 seconden", + "SCRIPT_LIST_GENERAL_2": "Espresso, eindeloos", + "SCRIPT_LIST_GENERAL_STOP": "Schot stoppen", + "CHOOSE_SCRIPT_AT_WEIGHT": "Kies script voor gewicht bereikt", + "ERROR_NOT_ALL_SCRIPTS_FOUND": "Een of meer scripts konden niet worden gevonden, ze zijn gereset", + "ERROR_CONNECTION_COULD_NOT_BE_ESTABLISHED": "De verbinding met de xenia espressomachine kon niet tot stand worden gebracht. Controleer of u zich op het juiste netwerk (LAN) bevindt, of u het juiste IP-adres hebt ingevoerd, etc.", + "CHECKING_CONNECTION_TO_PORTAFILTER": "Verbinding met Xenia wordt gecontroleerd", + "GRABING_SCRIPTS": "Scripts laden vanuit Xenia", + "BREW_BY_WEIGHT_ACTIVE": "Brouwen op gewicht actief" + }, + "API_VERSION": "Api-versie", + "RESIDUAL_LAG_TIME": "Resterende vertragingstijd", + "RESIDUAL_LAG_TIME_DESCRIPTION": "Stel de tijd in die overeenkomt met het resterende water. Met een naakte portafilter heb je een lagere tijd nodig, met een tuit een hogere. Hoe langzamer uw weegschaal het gewicht rapporteert, hoe meer tijd u nodig heeft", + "TYPE_METICULOUS": { + "TITLE": "Meticulous machine", + "NO_PROFILE": "Geen profiel", + "CHOOSE_PROFILE": "Profiel kiezen", + "SHOT_STARTED": "Brouwen gestart", + "SHOT_ENDED": "Brouwen gestopt" + } + }, + "DEVICE_CONNECTION": "Apparaatverbinding", + "PREPARATION_DEVICE_CONNECTION": "Apparaatverbinding", + "MANUAL_EXPORT_TO_VISUALIZER": "Exporteren naar Visualizer", + "ONLY_FAVOURITES": "Alleen favorieten", + "TEMPERATURE": { + "CONNECTION_NOT_ESTABLISHED": "Temperatuurapparaat niet gevonden, of verbinding kon niet tot stand worden gebracht", + "CONNECTED_SUCCESSFULLY": "Temperatuurapparaat aangesloten", + "DISCONNECTED_SUCCESSFULLY": "Temperatuurapparaat losgekoppeld", + "DISCONNECTED_UNPLANNED": "Temperatuurapparaat onverwachts losgekoppeld", + "STAY_CONNECTED_ON_APP_MINIMIZE": "Houd de thermometer verbonden, zelfs als de app op de achtergrond staat", + "INFORMATION_DESCRIPTION": "Ondersteunde apparaten zijn: ETI Ltd BLE-thermometers (ThermaQ Blue, BlueTherm, enz.), Combustion Inc., Meater (niet Meater+ of Meater 2)", + "BLUETOOTH_SCAN_RUNNING": "Zoeken naar temperatuurapparaat gedurende maximaal 60 seconden", + "BLUETOOTH_NOT_ENABLED": "Bluetooth niet geactiveerd, activeer het om het goed te laten werken", + "REQUEST_PERMISSION": { + "LOCATION": "Om temperatuurmeters te vinden, heeft de app toegang nodig tot de locatie.", + "BLUETOOTH": "Om temperatuurapparaten te vinden, heeft de app toegang tot Bluetooth nodig" + }, + "LOG": "Logbestanden voor temperatuurapparaat activeren", + "THRESHOLD_ACTIVE": "Start timer met vooraf ingestelde temperatuur", + "THRESHOLD_TEMP": "Drempel temperatuur" + }, + "POPOVER_BLUETOOTH_ACTION_RECONNECT_TEMPERATURE_DEVICE": "Temperatuurapparaat opnieuw aansluiten", + "PRESSURE_DEVICE_JUST_VISIBLE_ON_ESPRESSO": "Drukapparaat is alleen bruikbaar bij de bereidingswijze 'Espresso'", + "PRESSURE_MESSAGE_AFTER_CONNECTION": "Bekend gedrag (in analyse): Houd er rekening mee dat het langer kan duren om verbinding te maken nadat u uw telefoon opnieuw hebt opgestart of wanneer u de app langere tijd niet hebt gebruikt. Na deze tijd zou het verbindingsprobleem opgelost moeten zijn.", + "SMART_SCALE_ACAIA_HEARTBEAT_TIMER": "Hartslagtimer - Speciaal voor Acaia Scales", + "SMART_SCALE_ACAIA_HEARTBEAT_TIMER_TOOLTIP": "Alleen gebruikt voor Acaia-weegschalen! - Oudere Acaia-weegschalen hebben een hartslagsignaal nodig. Als u problemen ondervindt, probeer dan de hartslag op een hogere frequentie in te stellen.", + "SHARE_BEAN_URL": "Delen als URL", + "SHARE_BEAN_IMAGE": "Delen als afbeelding", + "SMART_SCALE_ESPRESSO_STOP_ON_NO_WEIGHT_CHANGE": "Espresso - Automatisch stoppen met brouwen", + "SMART_SCALE_ESPRESSO_STOP_ON_NO_WEIGHT_CHANGE_DESCRIPTION": "Deze instelling wordt alleen gebruikt voor brouwsels van het type 'espresso'. Het brouwsel wordt automatisch gestopt wanneer er geen flow wordt gedetecteerd.", + "BREW_DATA_BREW_QUANTITY_TOOLTIP_BREW_RATIO": "De zetverhouding wordt berekend met deze waarde voor alle brouwsels, maar niet voor type 'espresso'.", + "BREW_DATA_BREW_BEVERAGE_QUANTITY_TOOLTIP_BREW_RATIO": "De zetverhouding wordt alleen met deze waarde berekend voor brouwsels van het type 'espresso'", + "SMART_SCALE_DID_NOT_SEND_ANY_WEIGHT_DESCRIPTION": "Het lijkt erop dat de bluetooth weegschaal niet goed is aangesloten. Dit gebeurt meestal op iOS-apparaten met Acaia weegschalen, sluit de weegschaal opnieuw aan en zorg ervoor dat de gewichtstegel wordt bijgewerkt.", + "SMART_SCALE_DID_NOT_SEND_ANY_WEIGHT_TITLE": "Geen gewichtswaarden?", + "SMART_SCALE_ESPRESSO_STOP_ON_NO_WEIGHT_CHANGE_MIN_FLOW_DESCRIPTION": "De stroomsnelheid waarbij het zetten moet worden gestopt. Het zetten wordt pas gestopt als er minimaal 5 seconden zijn verstreken, er 5 gram in de kop zit of als u het gewicht van de gemalen koffie hebt ingevoerd - er is minimaal een zetverhouding van 1:1 bereikt (bijv.: 18 g erin, 18 g eruit). Nadat aan deze voorwaarden is voldaan, wordt de hier ingestelde minimale stroomsnelheid gecontroleerd", + "ONLY_BEST_BREWS": "Alleen de beste brouwsels", + "POPOVER_BEST_BREW": "Beste brouwsel", + "PAGE_SETTINGS_BEST_BREW": "Beste brouwsels activeren", + "PAGE_SETTINGS_BEST_BREW_DESCRIPTION": "Markeer je beste brouwsel voor een specifieke boon. Elke boon kan één beste brouwsel hebben in plaats van meerdere favorieten.", + "PAGE_SETTINGS_TAB_BLUETOOTH_REFRACTOMETER": "Refractometer apparaat", + "REFRACTOMETER": { + "CONNECTION_NOT_ESTABLISHED": "Refractometerapparaat niet gevonden, of verbinding kon niet tot stand worden gebracht", + "CONNECTED_SUCCESSFULLY": "Refractometerapparaat aangesloten", + "DISCONNECTED_SUCCESSFULLY": "Refractometerapparaat losgekoppeld", + "DISCONNECTED_UNPLANNED": "Refractometerapparaat onverwachts losgekoppeld", + "STAY_CONNECTED_ON_APP_MINIMIZE": "Houd de refractometer verbonden, zelfs als de app op de achtergrond draait", + "INFORMATION_DESCRIPTION": "Ondersteunde apparaten zijn: DiFluid R2", + "BLUETOOTH_SCAN_RUNNING": "Zoeken naar refractometer-apparaat gedurende maximaal 60 seconden", + "BLUETOOTH_NOT_ENABLED": "Bluetooth niet geactiveerd, activeer het om het goed te laten werken", + "REQUEST_PERMISSION": { + "LOCATION": "Om refractometerapparaten te vinden, heeft de app toegang nodig tot de locatie.", + "BLUETOOTH": "Om refractometerapparaten te vinden, heeft de app toegang tot Bluetooth nodig" + }, + "LOG": "Activeer logbestanden voor refractometerapparaat", + "READ_END": "Test voltooid - resultaat ontvangen" + }, + "COPIED_TO_CLIPBOARD_SUCCESSFULLY": "Toegevoegd aan klembord", + "COPIED_TO_CLIPBOARD_UNSUCCESSFULLY": "Kan niet worden toegevoegd aan het klembord", + "PAGE_SETTINGS_LANGUAGE_FRENCH": "Frans", + "ANDROID_EXTERNAL_FILE_ACCESS_NOT_POSSIBLE_TITLE": "Gegevens kunnen niet worden opgeslagen vanwege Android-beperkingen", + "ANDROID_EXTERNAL_FILE_ACCESS_NEEDED_DESCRIPTION": "Je Android-telefoon ondersteunt geen externe bestandssystemen, dus je moet het ZIP-bestand downloaden zonder mediabestanden. Ga voor meer informatie naar https:\/\/beanconqueror.com\/faq.", + "PAGE_SETTINGS_SECURITY_CHECK_WHEN_GOING_BACK": "Beveiligingsbericht voor afsluiten?", + "PAGE_SETTINGS_SECURITY_CHECK_WHEN_GOING_BACK_DESCRIPTION": "Controleer of de gegevens zijn gewijzigd bij het toevoegen\/bewerken van bonen of brouwsels; als dat het geval is, wordt er een veiligheidswaarschuwing weergegeven bij het teruggaan.", + "PAGE_BEANS_DISCARD_CONFIRM": "Boon informatie is gewijzigd. Weet je zeker dat je de pagina wilt verlaten zonder op te slaan?", + "PAGE_BREW_DISCARD_CONFIRM": "De brouw informatie is gewijzigd. Weet je zeker dat je de pagina wilt verlaten zonder op te slaan?", + "NO_ENTRIES_FOUND": "Geen vermeldingen gevonden", + "POPOVER_BEANS_OPTION_REPEAT": "Herhaal laatste brouwsel", + "REPEAT_LAST_BREW": "Herhaal laatste brouwsel", + "REPEAT_BEST_BREW": "Herhaal beste brouwsel", + "PAGE_SETTINGS_VISUALIZER_SECTION": "Visualizer", + "VISUALIZER": { + "ACTIVATE": "Visualizer activeren", + "CHOOSE_SERVER": "Server kiezen", + "CONNECTION": { + "UNSUCCESSFULLY": "Er kon geen verbinding tot stand worden gebracht.", + "SUCCESSFULLY": "Verbinding kon tot stand worden gebracht." + }, + "SERVER": { + "VISUALIZER": "Visualizer-server", + "CUSTOM": "Aangepaste server" + }, + "SHOT": { + "UPLOAD_SUCCESSFULLY": "Brouwen geüpload naar visualizer.", + "UPLOAD_UNSUCCESSFULLY": "Brouwen kon niet worden geüpload naar de Visualizer." + }, + "URL": "Server-URL", + "USERNAME": "Gebruikersnaam", + "PASSWORD": "Wachtwoord", + "UPLOAD_AUTOMATIC": "Elke brouwsessie automatisch uploaden?", + "UPLOAD_AUTOMATIC_TOOLTIP": "Zorg ervoor dat u een actieve internetverbinding hebt wanneer u uw brouwsel opslaat.", + "UPLOAD_ALL": "Alle brouwsels uploaden", + "NOT_ALL_SHOTS_UPLOADED": "Niet alle brouwsels konden worden geüpload", + "ALL_SHOTS_UPLOADED": "Alle brouwsels zijn geüpload" + }, + "SMART_SCALE_AUTO_START_LISTENING": "Timer automatisch starten?", + "SMART_SCALE_AUTO_START_LISTENING_DESCRIPTION": "Start de timer automatisch bij het bereiken van de volgende gewichtswaarde", + "CHOOSE_REFERENCE_GRAPH": "Referentiegrafiek selecteren", + "RESET": "Opnieuw instellen", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_AXIS": "Definieer de beginassen van de grafiek", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_AXIS_DESCRIPTION": "Stel de begingrootte van de assen in voor zowel filter- als espressokoffie.", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_FILTER_WEIGHT": "Filter - Gewicht", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_FILTER_FLOW": "Filter - Doorstroming", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_ESPRESSO_WEIGHT": "Espresso - Gewicht", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_ESPRESSO_FLOW": "Espresso - Doorstroming", + "SMART_SCALE_IGNORE_INCOMING_WEIGHT": "Negeer huidige gewichtsoverdracht ", + "SMART_SCALE_IGNORE_INCOMING_WEIGHT_TOOLTIP": "Er verschijnt een nieuwe knop in het brouwgedeelte, die nieuwe gewichtswaarden van de bluetooth weegschaal zal negeren.", + "BREWS_ACTIVE": "Actieve brouwsels", + "BREWS_ARCHIVED": "Gearchiveerde brouwsels", + "GRAPHS": "Grafieken", + "GRAPH_SECTION": { + "NAV_GRAPH": "Grafieken", + "NO_ARCHIVED_ENTRIES": "Geen gearchiveerde vermeldingen", + "NO_ENTRIES": "Geen vermeldingen", + "SECTION_HAS_BEEN_ACTIVATED": "Grafieksectie is geactiveerd" + }, + "TOAST_GRAPH_ARCHIVED_SUCCESSFULLY": "Grafiek gearchiveerd", + "TOAST_GRAPH_DELETED_SUCCESSFULLY": "Grafiek verwijderd", + "TOAST_GRAPH_EDITED_SUCCESSFULLY": "Grafiek bewerkt", + "TOAST_GRAPH_ADD_SUCCESSFULLY": "Grafiek toegevoegd", + "NAV_GRAPH_SECTION": "Grafieken", + "DELETE_GRAPH_QUESTION": "Wilt u deze grafiek verwijderen?", + "PAGE_SETTINGS_SHOW_ARCHIVED_GRAPHS": "Gearchiveerde grafieken weergeven", + "PAGE_SETTINGS_SHOW_GRAPH_SECTION": "Grafieksectie activeren", + "EDIT_GRAPH": "Grafiek bewerken", + "ADD_GRAPH": "Grafiek toevoegen", + "GRAPH": { + "PLACE_HOLDER": { + "NAME": "Grafiek naam", + "NOTES": "Notities" + }, + "UPLOAD": "Grafiek uploaden", + "DELETE": "Grafiek verwijderen", + "UPLOAD_DESCRIPTION": "Importeer een .JSON-bestand dat u kunt downloaden in de brew-detailweergave. U kunt ook gedeelde grafieken van de community importeren die van het type .JSON zijn." + }, + "SHOW_VISUALIZER": "Visualiseerder weergeven", + "NO_BREWS_FOUND": "Geen brouwsels gevonden", + "NO_GRAPHS_FOUND": "Geen grafieken gevonden", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_TIME_AXIS": "Definieer de maximale tijdsas van de grafiek", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_TIME_DESCRIPTION": "Stel het maximum aantal seconden in op de tijd-as voor zowel filterkoffie als espresso.", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_TIME_FILTER_AXIS_NORMAL_SCREEN": "Filter - Normaal scherm", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_TIME_FILTER_AXIS_FULL_SCREEN_SCREEN": "Filter - Volledig scherm", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_TIME_ESPRESSO_AXIS_NORMAL_SCREEN": "Espresso - Normaal scherm", + "PAGE_SETTINGS_TAB_BLUETOOTH_SCALE_GRAPHS_TIME_ESPRESSO_AXIS_FULL_SCREEN_SCREEN": "Espresso - Volledig scherm", + "PAGE_SETTINGS_DATE_FORMAT": "Datum notatie", + "PAGE_SETTINGS_LANGUAGE_FRANCE": "Frans", + "PAGE_SETTINGS_LANGUAGE_INDONESIA": "Indonesisch", + "EXTRACTION_CHART_TITLE": "Extractie grafiek", + "PAGE_SETTINGS_SHOW_BACKUP_ISSUES": "Back-up problemen weergeven", + "PAGE_SETTINGS_SHOW_BACKUP_ISSUES_DESCRIPTION": "Geef een pop-up weer als er geen back-ups naar het bestandssysteem kunnen worden geschreven", + "AUTOMATIC_BACKUP_DID_FAIL": "Automatische back-up werkte niet, zorg ervoor dat u werkende back-ups hebt! Deze informatie kan worden uitgeschakeld in de instellingen", + "INTERNAL_BACKUP_DID_FAIL": "Interne back-up werkte niet, zorg ervoor dat u werkende back-ups hebt! Deze informatie kan worden uitgeschakeld in de instellingen", + "ZIP_BACKUP_FILE_COULD_NOT_BE_BUILD": "ZIP-bestand kon niet worden opgeslagen! Deze informatie kan worden uitgeschakeld in de instellingen", + "SEND_LOGS": "Logboeken verzenden", + "POPOVER_BLUETOOTH_ACTION_RECONNECT_REFRACTOMETER": "Sluit refractometer apparaat opnieuw aan", + "PAGE_SETTINGS_LANGUAGE_ITALIAN": "Italiaans", + "PAGE_SETTINGS_LANGUAGE_POLISH": "Pools", + "BREW_CANT_START_BECAUSE_TIMER_NOT_RESETTED_GENERAL_DESCRIPTION": "Je moet eerst je timer resetten voordat je kunt beginnen.", + "SMART_SCALE_FIRST_DRIP_THRESHOLD": "Eerste druppel drempel", + "SMART_SCALE_FIRST_DRIP_THRESHOLD_TOOLTIP": "Bij welk weegschaalgewicht moet de eerste druppel worden geteld? Standaard: >= 0,1g", + "PAGE_SETTINGS_BREW_TIMER_START_DELAY_ACTIVE": "Zetvertraging starten", + "PAGE_SETTINGS_BREW_TIMER_START_DELAY_ACTIVE_DESCRIPTION": "Stel een vertragingstijd in die een laadspinner weergeeft totdat het brouwen begint", + "STARTING_IN": "Beginnend in ... {{time}}", + "IOS_DATABASE_ISSUE_TITLE": "ATTENTIE !!!!! - DATABASEVERBINDING VERLOREN", + "IOS_DATABASE_ISSUE_DESCRIPTION": "Het spijt ons u te moeten meedelen dat de verbinding met de database is verbroken. Dit probleem is het gevolg van een onopgeloste bug in het systeem van Apple, waarover Beanconqueror geen controle heeft. Om mogelijk gegevensverlies te voorkomen, verzoeken wij u vriendelijk de Beanconqueror-applicatie onmiddellijk geforceerd af te sluiten en opnieuw te openen.", + "RELOAD_APP": "Herstart de app", + "WATER_TYPE_ADD_CUSTOM": "Water op maat", + "WATER_TYPE_THIRD_WAVE_WATER_CLASSIC_LIGHT_ROAST_PROFILE": "Third Wave Water - Klassiek licht geroosterd profiel", + "WATER_TYPE_THIRD_WAVE_WATER_MEDIUM_ROAST_PROFILE": "Third Wave Water - Middel geroosterd profiel", + "WATER_TYPE_THIRD_WAVE_WATER_DARK_ROAST_PROFILE": "Third Wave Water - Donker geroosterd profiel", + "WATER_TYPE_THIRD_WAVE_WATER_ESPRESSO_MACHINE_PROFILE": "Third Wave Water - Espressomachine Profiel", + "WATER_TYPE_THIRD_WAVE_WATER_COLD_BREW_PROFILE": "Third Wave Water - koud brouwprofiel", + "WATER_TYPE_THIRD_WAVE_WATER_LOW_ACID_PROFILE": "Third Wave Water - Laag Zuurprofiel", + "ADD_WATER": "Voeg water toe", + "EXTRACTION_CHART_LABEL_STRONG_UNDEREXTRACTED": "STERK
onderextractie", + "EXTRACTION_CHART_LABEL_STRONG": "Strong
", + "EXTRACTION_CHART_LABEL_STRONG_HARSH": "STERK
ruw", + "EXTRACTION_CHART_LABEL_UNDEREXTRACTED": "ondergeëxtraheerd", + "EXTRACTION_CHART_LABEL_IDEAL": "IDEAAL", + "EXTRACTION_CHART_LABEL_HARSH": "ruw", + "EXTRACTION_CHART_LABEL_WEAK_UNDEREXTRACTED": "ZWAK
onderextractie", + "EXTRACTION_CHART_LABEL_WEAK": "ZWAK
", + "EXTRACTION_CHART_LABEL_WEAK_HARSH": "ZWAK
ruw", + "PAGE_SETTINGS_TEXT_TO_SPEECH_SECTION": "Tekst naar spraak", + "TEXT_TO_SPEECH": { + "ACTIVATE": "Activeer tekst naar spraak", + "BREW_STARTED": "Brouwen gestart", + "BREW_ENDED": "Einde brouwen", + "TIME": "Tijd", + "SPEAK_EVERY_MS": "Spreek elke geselecteerde milliseconde", + "FOLLOWING_NUMBERS_WILL_BE_TEST_SPOKEN": "De volgende nummers worden als test uitgesproken", + "TEST_SPEAK": "Start test spraak", + "PITCH": "Toonhoogte", + "RATE": "beoordeling" + }, + "PAGE_SETTINGS_HAPTIC_FEEDBACK_SECTION": "Haptische feedback", + "HAPTIC_FEEDBACK": { + "ACTIVATE": "Activeer haptische feedback", + "BREW_STARTED": "Trillen bij het starten van het brouwen", + "BREW_STOPPED": "Trillen bij het stoppen van het brouwen", + "TARE": "Trillen bij tarra van de weegschaal" + }, + "CONNECTED": "Verbonden", + "DISCONNECTED": "Losgekoppeld", + "EXPERIMENTAL_FEATURE_DISCLAIMER": "Dit is een experimentele functie", + "SHOW_HOURS": "Toon uren", + "SHOW_MINUTES": "Toon Minuten", + "BEANS_UNARCHIVE": "uit het archief halen", + "TOAST_BEAN_UNARCHIVED_SUCCESSFULLY": "Boon is uit het archief gehaald", + "WATER_TYPE_PURE_COFFEE_WATER": "Zuiver Koffiewater", + "WATER_TYPE_EMPIRICAL_WATER_GLACIAL": "empirical water GLACIAL", + "WATER_TYPE_EMPIRICAL_WATER_SPRING": "empirical water SPRING", + "SORT_PREPARATION_TOOLS": "Sorteer bereidingsmiddelen", + "PREPARATION_TYPE_METICULOUS": "Meticulous", + "EXPORT_CAUTION": "Exporteren exporteert alleen de database, geen afbeeldingen, geen stromingsprofielen . Als deze nodig zijn, ga dan naar Gitbook voor meer informatie", + "POPOVER_FREEZE_COFFEE_BEAN": "Bevries koffieboon", + "POPOVER_UNFREEZE_COFFEE_BEAN": "Koffieboon ontdooien", + "BEAN_POPOVER_EDIT_FREEZE_DATE": "invries datum bewerken", + "BEAN_POPOVER_EDIT_UNFREEZE_DATE": "ontdooi datum bewerken", + "BEAN_POPOVER_LEFT_UNFROZEN": "Niet ingevroren", + "BEAN_POPOVER_FREEZE_PARTIAL_BAG": "Gedeelte zak invriezen (g)", + "FROZEN_BEANS": "Bevroren", + "BEAN_TAB_FROZEN_INFORMATION": "Bevroren notities", + "BEAN_POPOVER_FROZEN_BAGS": "Bevroren zakken", + "BEAN_POPOVER_FROZEN_DELETE_BEAN_MESSAGE": "Je gaat de hele zak invriezen en we hebben geen brouwsels gevonden die ermee gemaakt zijn, wil je de originele zak verwijderen?", + "CREATE_FROZEN_BEANS": "creëer bonen", + "BEAN_POPOVER_COPY_ATTACHMENTS": "Kopieer bijlagen", + "BEAN_POPOVER_COPY_ATTACHMENTS_DESCRIPTION": "Het kopiëren van bijlagen is aan het begin van deze functie gedeactiveerd", + "BEAN_DATA_FROZEN_DATE": "Invries datum", + "BEAN_DATA_UNFROZEN_DATE": "ontdooi datum", + "PAGE_BEANS_LIST_YOU_GOT_NO_FROZEN_BEANS": "Je hebt geen ingevroren bonen", + "BEAN_DATA_FROZEN_ID": "Ingevroren id", + "PAGE_SETTINGS_MANAGE_FEATURES": "Functies beheren", + "ACTIVE_BEAN_FREEZING_FEATURE": "Activeer het invriezen van bonen", + "NAV_FROZEN_BEANS_LIST": "Lijst met ingevroren bonen", + "BEAN_BREW_LIST_VIEW_PARAMETERS": "Boon informatie voor brouwen", + "BEAN_AGE_BY_BREW_DATE": "Leeftijd van de bonen op brouwdatum", + "BEAN_POPOVER_FROZEN_BEAN_WILL_BE_ARCHIVED_NOW_MESSAGE": "Je diepvrieszakken hebben als resultaat dat je originele zak geen gewicht meer heeft, maar je hebt er wel mee gebrouwen, dus beoordeel en stuur naar het archief.", + "BEAN_POPOVER_YOU_CANT_FREEZE_WITH_ZERO_WEIGHT_LEFT": "Je kunt niet bevriezen, want het restgewicht van je zak is nul", + "BEAN_DATA_BEST_DATE": "Beste Bonen Datum", + "BEAN_DATA_BEST_DATE_TOOLTIP": "Wanneer is de beste datum om de bonen te gebruiken?", + "BEAN_DATA_OPEN_DATE": "Openingsdatum van de zak", + "BEAN_DATA_OPEN_DATE_TOOLTIP": "Wanneer heb je de zak bonen geopend?", + "BEAN_FREEZING_STORAGE_TYPE_UNKNOWN": "Onbekend", + "BEAN_FREEZING_STORAGE_TYPE_COFFEE_BAG": "Koffiezak", + "BEAN_FREEZING_STORAGE_TYPE_COFFEE_JAR": "Koffie pot", + "BEAN_FREEZING_STORAGE_TYPE_ZIP_LOCK": "Ritssluiting", + "BEAN_FREEZING_STORAGE_TYPE_VACUUM_SEALED": "Vacuüm verzegeld", + "BEAN_FREEZING_STORAGE_TYPE_TUBE": "Buis", + "BEAN_DATA_FROZEN_STORAGE_TYPE": "Vriezer opslagtype", + "PAGE_SETTINGS_BREW_RATING_CHANGED_BREWS_NOT_VISIBLE": "U heeft de maximale beoordeling van uw brouwsels gewijzigd, maar u heeft uw brouwsels al hoger beoordeeld dan uw werkelijke maximum. Deze worden dus niet weergegeven bij de normale filterinstellingen.", + "PAGE_SETTINGS_BEAN_RATING_CHANGED_BEANS_NOT_VISIBLE": "U heeft de maximale beoordeling voor bonen gewijzigd, maar u heeft bonen al hoger beoordeeld dan uw werkelijke maximum. Ze worden dus niet weergegeven met de normale filterinstellingen.", + "BEAN_DATA_FROZEN_NOTE": "Bevroren notities", + "IGNORE_NEGATIVE_VALUES_DESCRIPTION": "Als je dit activeert, loopt de grafiek één seconde achter", + "IGNORE_ANOMALY_VALUES_DESCRIPTION": "Als je dit activeert, loopt de grafiek één seconde achter", + "SAVE_LOGFILES_TO_NOTES_FROM_MACHINE": "Bewaar de machine logs bij het opslaan van een brouwsel" +} \ No newline at end of file diff --git a/src/assets/i18n/pl.json b/src/assets/i18n/pl.json index ca541d8e2..23e94e5e6 100644 --- a/src/assets/i18n/pl.json +++ b/src/assets/i18n/pl.json @@ -1273,39 +1273,6 @@ "SHOW_MINUTES": "Pokaż minuty", "BEANS_UNARCHIVE": "Cofnij archiwizację", "TOAST_BEAN_UNARCHIVED_SUCCESSFULLY": "Ziarno nie zostało zarchiwizowane", - "UPDATE_TEXT_TITLE_TITLE": { - "7.4.0": { - "TITLE": "Wersja 7.4.0: Co nowego", - "DESCRIPTION": [ - "Ziarna<\/b>", - "Teraz możliwe jest zamrożenie ziaren kawy, aktywując tę funkcję w ustawieniach.", - "Dodano najlepszą i otwartą datę dla fasoli, należy aktywować ten parametr", - "Ziarna można teraz dodawać bezpośrednio z przeglądu wyboru.", - "", - "Termometr<\/b>", - "Wsparcie Combustion Inc. Termometr - Dziękujemy za urządzenie!", - "Wsparcie dla termometru Meater (nie Meater 2 lub Meater +) - dzięki Yannick!", - "", - "Sekcja wodna<\/b>", - "Dodano czystą wodę kawową", - "Dodana woda empiryczna", - "", - "Narzędzia przygotowawcze<\/b>", - "Posortuj teraz swoje narzędzia przygotowawcze", - "", - "Młynek<\/b>", - "Obrazy młynka są teraz wyświetlane w przeglądzie wyboru", - "", - "Ustawienia<\/b>", - "Komunikat bezpieczeństwa, jeśli zmieniono ocenę dla naparów lub ziaren, a maksymalna ocena jest niższa niż już oceniony wpis.", - "", - "Inne<\/b>", - "Przywrócono bezpośrednie ustawianie ostrości w sekundach podczas otwierania timera.", - "Kilka zmian technicznych w kodzie", - "Drobne poprawki" - ] - } - }, "WATER_TYPE_PURE_COFFEE_WATER": "Czysta woda kawowa", "WATER_TYPE_EMPIRICAL_WATER_GLACIAL": "woda empiryczna GLACIAL", "WATER_TYPE_EMPIRICAL_WATER_SPRING": "woda empiryczna SPRING", diff --git a/src/assets/i18n/tr.json b/src/assets/i18n/tr.json index cfb0c409b..f2e491740 100644 --- a/src/assets/i18n/tr.json +++ b/src/assets/i18n/tr.json @@ -1273,39 +1273,6 @@ "SHOW_MINUTES": "Dakikayı göster", "BEANS_UNARCHIVE": "Arşivden çıkar", "TOAST_BEAN_UNARCHIVED_SUCCESSFULLY": "Çekirdek arşivden çıkarıldı", - "UPDATE_TEXT_TITLE_TITLE": { - "7.4.0": { - "TITLE": "Sürüm 7.4.0: Yenilikler", - "DESCRIPTION": [ - " Fasulye <\/b>", - "Artık kahve çekirdeklerinizi dondurmak mümkün, bu özelliği ayarlardan etkinleştirin", - "Çekirdek için en iyi ve açık tarih eklendi, bu parametreyi etkinleştirmeniz gerekir", - "Çekirdekler artık doğrudan seçime genel bakıştan eklenebilir", - "", - "Termometre<\/b>", - "Combustion Inc. Termometre Desteği - Cihaz için teşekkürler!", - "Meater Termometre desteği (Meater 2 veya Meater + değil) - Yannick'e teşekkürler!", - "", - "Su Bölümü<\/b>", - "Saf Kahve Suyu Eklendi", - "Ampirik su eklendi", - "", - "Hazırlık Araçları<\/b>", - "Hazırlık araçlarınızı şimdi sıralayın", - "", - "Öğütücü<\/b>", - "Öğütücü görüntüleri artık seçim genel görünümünde gösteriliyor", - "", - "Ayarlar<\/b>", - "Demleme veya çekirdek derecelendirmesini değiştirdiyseniz ve maksimum değer, halihazırda derecelendirilmiş bir girişten düşükse güvenlik mesajı", - "", - "Diğer<\/b>", - "Zamanlayıcıyı açarken doğrudan saniye odağı geri döndürüldü", - "Koddaki bazı teknik değişiklikler", - "Küçük değişiklikler" - ] - } - }, "WATER_TYPE_PURE_COFFEE_WATER": "Saf Kahve Suyu", "WATER_TYPE_EMPIRICAL_WATER_GLACIAL": "ampirik su GLACIAL", "WATER_TYPE_EMPIRICAL_WATER_SPRING": "ampirik su SPRING", diff --git a/src/assets/i18n/zh.json b/src/assets/i18n/zh.json index b7c5bd637..bca45dc75 100644 --- a/src/assets/i18n/zh.json +++ b/src/assets/i18n/zh.json @@ -1273,39 +1273,6 @@ "SHOW_MINUTES": "显示分钟", "BEANS_UNARCHIVE": "取消归档", "TOAST_BEAN_UNARCHIVED_SUCCESSFULLY": "咖啡豆已经取消归档", - "UPDATE_TEXT_TITLE_TITLE": { - "7.4.0": { - "TITLE": "版本7.4.0:有什么新功能?", - "DESCRIPTION": [ - "豆子<\/b>", - "现在可以将您的咖啡豆冷冻保存,您可以在设置中激活这个功能", - "为咖啡豆添加了最佳和开封日期,您需要激活此参数", - "现在可以直接从选择概览中添加豆子", - "空的", - "温度计<\/b>", - "支持 Combustion Inc. 温度计 - 感谢提供设备!", - "支持 Meater 温度计(不支持 Meater 2 或 Meater +) - 感谢 Yannick!", - "空的", - "水分区<\/b>", - "添加Pure Coffee Water", - "添加Empirical水", - "空的", - "准备工具<\/b>", - "整理您的准备工具", - "空的", - "磨豆机<\/b>", - "磨豆机的图片现在在选择概览中显示", - "空的", - "设置<\/b>", - "如果您更改了冲泡或咖啡豆的评分,并且最高评分低于已评分条目的评分,则显示安全提示信息", - "空的", - "其他<\/b>", - "在打开计时器时,恢复到以秒数计时为准", - "代码中的一些技术修改", - "小调整" - ] - } - }, "WATER_TYPE_PURE_COFFEE_WATER": "Pure Coffee Water", "WATER_TYPE_EMPIRICAL_WATER_GLACIAL": "empirical water GLACIAL", "WATER_TYPE_EMPIRICAL_WATER_SPRING": "empirical water SPRING", diff --git a/src/classes/bean/bean.ts b/src/classes/bean/bean.ts index 8c0b28a6d..633b48a3a 100755 --- a/src/classes/bean/bean.ts +++ b/src/classes/bean/bean.ts @@ -156,6 +156,13 @@ export class Bean implements IBean { return false; } } + public isUnfrozen() { + if (this.frozenDate && this.unfrozenDate) { + return true; + } else { + return false; + } + } public initializeByObject(beanObj: IBean): void { Object.assign(this, beanObj); diff --git a/src/classes/devices/argosThermometer.ts b/src/classes/devices/argosThermometer.ts new file mode 100644 index 000000000..8957aadc3 --- /dev/null +++ b/src/classes/devices/argosThermometer.ts @@ -0,0 +1,75 @@ +import { PeripheralData } from './ble.types'; +import { Logger } from './common/logger'; + +import { TemperatureDevice } from './temperatureBluetoothDevice'; + +declare var ble: any; + +export class ArgosThermometer extends TemperatureDevice { + public static DEVICE_NAME = 'ARGOS'; + public static TEMPERATURE_SERVICE_UUID = + '6a521c59-55b5-4384-85c0-6534e63fb09e'; + public static TEMPERATURE_CHAR_UUID = '6a521c62-55b5-4384-85c0-6534e63fb09e'; + + private logger: Logger; + + constructor(data: PeripheralData) { + super(data); + this.connect(); + this.logger = new Logger('ArgosTemperatureSensor'); + } + + public static test(device: any): boolean { + return ( + device && + device.name && + device.name + .toLowerCase() + .startsWith(ArgosThermometer.DEVICE_NAME.toLowerCase()) + ); + } + + public connect() { + this.attachNotification(); + } + + public disconnect() { + this.deattachNotification(); + } + + private attachNotification() { + ble.startNotification( + this.device_id, + ArgosThermometer.TEMPERATURE_SERVICE_UUID, + ArgosThermometer.TEMPERATURE_CHAR_UUID, + + async (_data: any) => { + let newData = new Uint8Array(_data.slice(0, -1)); + this.parseStatusUpdate(newData); + }, + + (_data: any) => {} + ); + } + + private parseStatusUpdate(temperatureRawStatus: Uint8Array) { + this.logger.log( + 'temperatureRawStatus received is: ' + temperatureRawStatus + ); + + const temperature_in_f = + temperatureRawStatus[-1] << (8 + temperatureRawStatus[-2]); + console.log('New temperature inc' + temperature_in_f); + this.setTemperature(temperature_in_f, temperatureRawStatus); + } + + private deattachNotification() { + ble.stopNotification( + this.device_id, + ArgosThermometer.TEMPERATURE_SERVICE_UUID, + ArgosThermometer.TEMPERATURE_CHAR_UUID, + (e: any) => {}, + (e: any) => {} + ); + } +} diff --git a/src/classes/devices/bokooScale.ts b/src/classes/devices/bokooScale.ts index ad3144985..a20520730 100644 --- a/src/classes/devices/bokooScale.ts +++ b/src/classes/devices/bokooScale.ts @@ -49,6 +49,16 @@ export class BookooScale extends BluetoothScale { await this.write(new Uint8Array([0x03, 0x0a, 0x01, 0x00, 0x00, 0x08])); } + public async tareAndStartTimerModeAuto() { + this.weight.smoothed = 0; + this.weight.actual = 0; + this.weight.oldSmoothed = 0; + this.weight.old = 0; + this.setWeight(0); + + await this.write(new Uint8Array([0x03, 0x0a, 0x07, 0x00, 0x00, 0x00])); + } + public override disconnectTriggered(): void { this.logger.log('Disconnecting...'); this.deattachNotification(); diff --git a/src/classes/devices/index.ts b/src/classes/devices/index.ts index b18f3a7e9..34c3e63b5 100644 --- a/src/classes/devices/index.ts +++ b/src/classes/devices/index.ts @@ -25,6 +25,7 @@ import { BookooScale } from './bokooScale'; import { BasicGrillThermometer } from './basicGrillThermometer'; import { MeaterThermometer } from './meaterThermometer'; import { CombustionThermometer } from './combustionThermometer'; +import { ArgosThermometer } from './argosThermometer'; export { BluetoothScale, SCALE_TIMER_COMMAND } from './bluetoothDevice'; export * from './common'; @@ -56,6 +57,7 @@ export enum TemperatureType { BASICGRILL = 'BASICGRILL', MEATER = 'MEATER', COMBUSTION = 'COMBUSTION', + ARGOS = 'ARGOS', } export enum RefractometerType { @@ -129,6 +131,8 @@ export function makeTemperatureDevice( return new MeaterThermometer(data); case TemperatureType.COMBUSTION: return new CombustionThermometer(data); + case TemperatureType.ARGOS: + return new ArgosThermometer(data); default: return null; } diff --git a/src/classes/preparation/preparation.ts b/src/classes/preparation/preparation.ts index d18e46396..299f7a2c2 100755 --- a/src/classes/preparation/preparation.ts +++ b/src/classes/preparation/preparation.ts @@ -12,6 +12,7 @@ import { UIHelper } from '../../services/uiHelper'; import { ListViewBrewParameter } from '../parameter/listViewBrewParameter'; import { RepeatBrewParameter } from '../parameter/repeatBrewParameter'; import { ConnectedPreparationDevice } from '../preparationDevice/connectedPreparationDevice'; +import { PreparationDeviceType } from '../preparationDevice'; export class Preparation implements IPreparation { public name: string; @@ -163,6 +164,10 @@ export class Preparation implements IPreparation { return PREPARATION_STYLE_TYPE.POUR_OVER; case PREPARATION_TYPES.METICULOUS: return PREPARATION_STYLE_TYPE.ESPRESSO; + case PREPARATION_TYPES.XENIA: + return PREPARATION_STYLE_TYPE.ESPRESSO; + case PREPARATION_TYPES.SANREMO_YOU: + return PREPARATION_STYLE_TYPE.ESPRESSO; default: return PREPARATION_STYLE_TYPE.POUR_OVER; } @@ -249,6 +254,8 @@ export class Preparation implements IPreparation { return 'beanconqueror-preparation-tricolate'; case PREPARATION_TYPES.METICULOUS: return 'beanconqueror-preparation-meticulous'; + case PREPARATION_TYPES.SANREMO_YOU: + return 'beanconqueror-preparation-sanremo-you'; default: return 'beanconqueror-preparation-custom'; } @@ -305,4 +312,7 @@ export class Preparation implements IPreparation { public hasPhotos(): boolean { return this.attachments && this.attachments.length > 0; } + public hasDeviceConnection(): boolean { + return this.connectedPreparationDevice?.type !== PreparationDeviceType.NONE; + } } diff --git a/src/classes/preparationDevice/index.ts b/src/classes/preparationDevice/index.ts index ad1960ce2..729697c2f 100644 --- a/src/classes/preparationDevice/index.ts +++ b/src/classes/preparationDevice/index.ts @@ -3,11 +3,13 @@ import { PreparationDevice } from './preparationDevice'; import { HttpClient } from '@angular/common/http'; import { Preparation } from '../preparation/preparation'; import { MeticulousDevice } from './meticulous/meticulousDevice'; +import { SanremoYOUDevice } from './sanremo/sanremoYOUDevice'; export enum PreparationDeviceType { NONE = 'NONE', XENIA = 'XENIA', METICULOUS = 'METICULOUS', + SANREMO_YOU = 'SANREMO_YOU', } export function makePreparationDevice( @@ -20,6 +22,8 @@ export function makePreparationDevice( return new XeniaDevice(_http, _preparation); case PreparationDeviceType.METICULOUS: return new MeticulousDevice(_http, _preparation); + case PreparationDeviceType.SANREMO_YOU: + return new SanremoYOUDevice(_http, _preparation); default: return null; } diff --git a/src/classes/preparationDevice/meticulous/meticulousDevice.ts b/src/classes/preparationDevice/meticulous/meticulousDevice.ts index ee5022dec..25a0daf64 100644 --- a/src/classes/preparationDevice/meticulous/meticulousDevice.ts +++ b/src/classes/preparationDevice/meticulous/meticulousDevice.ts @@ -1,15 +1,22 @@ import { PreparationDevice } from '../preparationDevice'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Preparation } from '../../preparation/preparation'; import { MeticulousShotData } from './meticulousShotData'; -import Api, { - ApiResponseError, - ProfileIdent, - ActionType, -} from 'meticulous-api'; +import Api, { ActionType, ProfileIdent } from '@meticulous-home/espresso-api'; import { IMeticulousParams } from '../../../interfaces/preparationDevices/meticulous/iMeticulousParams'; import { Profile } from 'meticulous-typescript-profile'; +import { catchError, timeout } from 'rxjs/operators'; +import { of } from 'rxjs'; +import { HistoryListingEntry } from '@meticulous-home/espresso-api/dist/types'; +import moment from 'moment'; +import { + BrewFlow, + IBrewPressureFlow, + IBrewRealtimeWaterFlow, + IBrewTemperatureFlow, + IBrewWeightFlow, +} from '../../brew/brewFlow'; declare var cordova; declare var io; @@ -22,6 +29,55 @@ export class MeticulousDevice extends PreparationDevice { private _profiles: Array = []; + private serverURL: string = ''; + + public static returnBrewFlowForShotData(_shotData) { + const newMoment = moment(new Date()).startOf('day'); + const newBrewFlow = new BrewFlow(); + + for (const entry of _shotData as any) { + const shotEntry: any = entry.shot; + const shotEntryTime = newMoment.clone().add('milliseconds', entry.time); + const timestamp = shotEntryTime.format('HH:mm:ss.SSS'); + + const realtimeWaterFlow: IBrewRealtimeWaterFlow = + {} as IBrewRealtimeWaterFlow; + + realtimeWaterFlow.brew_time = ''; + realtimeWaterFlow.timestamp = timestamp; + realtimeWaterFlow.smoothed_weight = 0; + realtimeWaterFlow.flow_value = shotEntry.flow; + realtimeWaterFlow.timestampdelta = 0; + + newBrewFlow.realtimeFlow.push(realtimeWaterFlow); + + const brewFlow: IBrewWeightFlow = {} as IBrewWeightFlow; + brewFlow.timestamp = timestamp; + brewFlow.brew_time = ''; + brewFlow.actual_weight = shotEntry.weight; + brewFlow.old_weight = 0; + brewFlow.actual_smoothed_weight = 0; + brewFlow.old_smoothed_weight = 0; + brewFlow.not_mutated_weight = 0; + newBrewFlow.weight.push(brewFlow); + + const pressureFlow: IBrewPressureFlow = {} as IBrewPressureFlow; + pressureFlow.timestamp = timestamp; + pressureFlow.brew_time = ''; + pressureFlow.actual_pressure = shotEntry.pressure; + pressureFlow.old_pressure = 0; + newBrewFlow.pressureFlow.push(pressureFlow); + + const temperatureFlow: IBrewTemperatureFlow = {} as IBrewTemperatureFlow; + temperatureFlow.timestamp = timestamp; + temperatureFlow.brew_time = ''; + temperatureFlow.actual_temperature = shotEntry.temperature; + temperatureFlow.old_temperature = 0; + newBrewFlow.temperatureFlow.push(temperatureFlow); + } + return newBrewFlow; + } + constructor(protected httpClient: HttpClient, _preparation: Preparation) { super(httpClient, _preparation); this.meticulousShotData = undefined; @@ -29,10 +85,53 @@ export class MeticulousDevice extends PreparationDevice { undefined, _preparation.connectedPreparationDevice.url ); + this.serverURL = _preparation.connectedPreparationDevice.url; if (typeof cordova !== 'undefined') { } } + public getHistory() { + const promise = new Promise((resolve, reject) => { + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + }), + }; + this.httpClient + .post( + this.serverURL + '/api/v1/history', + { + sort: 'desc', + max_results: 20, + }, + httpOptions + ) + .pipe( + timeout(10000), + catchError((e) => { + return of(null); + }) + ) + .toPromise() + .then( + (data: any) => { + console.log(data); + if (data && data.history) { + resolve(data.history); + } + }, + (error) => { + console.log(error); + reject(); + } + ) + .catch((error) => { + console.log(error); + reject(); + }); + }); + return promise; + } public getActualShotData() { return this.meticulousShotData; @@ -45,24 +144,44 @@ export class MeticulousDevice extends PreparationDevice { try { const profileResponse = await this.metApi.getProfile(_profileId); - if (profileResponse instanceof ApiResponseError) { + const profile = profileResponse.data as unknown as Profile; + return profile; + /* if (profileResponse instanceof Profile) { } else { - const profile = profileResponse.data as unknown as Profile; - return profile; - } + + }*/ } catch (ex) {} return undefined; } + public async getHistoryShortListing() { + const response = await this.metApi.getHistoryShortListing(); + if (response && response.data && response.data.history) { + return response.data.history as Array; + } + } + + public async searchHistory() { + const response = await this.metApi.searchHistory({ + query: '', + ids: [], + start_date: '', + end_date: '', + order_by: ['date'], + sort: 'desc', + max_results: 10, + dump_data: true, + }); + if (response && response.data && response.data.history) { + return response.data.history as Array; + } + } + public async loadProfileByID(_profileId: string) { try { const loadProfile = await this.metApi.loadProfileByID(_profileId); - - if (loadProfile instanceof ApiResponseError) { - } else { - const profile = loadProfile.data as unknown as Profile; - return profile; - } + const profile = loadProfile.data as unknown as Profile; + return profile; } catch (ex) { console.log(ex.message); } @@ -87,11 +206,7 @@ export class MeticulousDevice extends PreparationDevice { try { if (this._profiles.length <= 0) { const profiles = await this.metApi.listProfiles(); - - if (profiles instanceof ApiResponseError) { - } else { - this._profiles = profiles.data as Array; - } + this._profiles = profiles.data as Array; } } catch (ex) {} } @@ -101,8 +216,9 @@ export class MeticulousDevice extends PreparationDevice { } public override async deviceConnected(): Promise { - const promise = new Promise((resolve, reject) => { - if (this.socket !== undefined) { + const promise = new Promise(async (resolve, reject) => { + const settings: any = await this.metApi.getSettings(); + if (settings?.data?.config) { resolve(true); } else { resolve(false); diff --git a/src/classes/preparationDevice/sanremo/sanremoYOUDevice.ts b/src/classes/preparationDevice/sanremo/sanremoYOUDevice.ts new file mode 100644 index 000000000..4cf6023ff --- /dev/null +++ b/src/classes/preparationDevice/sanremo/sanremoYOUDevice.ts @@ -0,0 +1,186 @@ +import { PreparationDevice } from '../preparationDevice'; +import { HttpClient } from '@angular/common/http'; +import { Preparation } from '../../preparation/preparation'; + +import { ISanremoYOUParams } from '../../../interfaces/preparationDevices/sanremoYOU/iSanremoYOUParams'; +import { SanremoYOUMode } from '../../../enums/preparationDevice/sanremo/sanremoYOUMode'; +declare var cordova; +export class SanremoYOUDevice extends PreparationDevice { + public scriptList: Array<{ INDEX: number; TITLE: string }> = []; + + private connectionURL: string = ''; + constructor(protected httpClient: HttpClient, _preparation: Preparation) { + super(httpClient, _preparation); + + this.connectionURL = this.getPreparation().connectedPreparationDevice.url; + } + + public async deviceConnected(): Promise { + const promise = new Promise((resolve, reject) => { + const options = { + method: 'get', + }; + let urlAdding = '/api/runtime'; + + cordova.plugin.http.sendRequest( + this.connectionURL + urlAdding, + options, + (response) => { + try { + const parsedJSON = JSON.parse(response.data); + resolve(true); + return; + + if (parsedJSON && 'id' in parsedJSON) { + resolve(true); + } else { + reject(''); + } + } catch (e) { + // alert("Error in Resolve " + JSON.stringify(e)); + reject(JSON.stringify(e)); + } + }, + (response) => { + // prints 403 + // alert("Error " + JSON.stringify(response)); + reject(JSON.stringify(response)); + } + ); + }); + return promise; + } + + public getPressure() { + return this.pressure; + } + + public getResidualLagTime() { + const connectedPreparationDevice = + this.getPreparation().connectedPreparationDevice; + if ( + connectedPreparationDevice.customParams && + connectedPreparationDevice.customParams.residualLagTime + ) { + return connectedPreparationDevice.customParams.residualLagTime; + } else { + // Fixed value. + return 1.35; + } + } + + public getSaveLogfilesFromMachine(): boolean { + const connectedPreparationDevice = + this.getPreparation().connectedPreparationDevice; + if ( + connectedPreparationDevice.customParams && + connectedPreparationDevice.customParams.saveLogfilesFromMachine + ) { + return connectedPreparationDevice.customParams.saveLogfilesFromMachine; + } else { + // Fixed value. + return false; + } + } + + public getTemperature() { + return this.temperature; + } + public getDevicetemperature() { + return this.deviceTemperature; + } + + public fetchRuntimeData(_callback: any = null) { + const options = { + method: 'get', + }; + const urlAdding = '/api/runtime'; + + cordova.plugin.http.sendRequest( + this.connectionURL + urlAdding, + options, + (response) => { + try { + /**{"status":1,"description":"ON","statusPhase":0,"alarms":0,"warnings":2,"tempBoilerCoffe":82.1,"tempBolierServices":100.2,"pumpServicesPress":0.2,"pumpPress":0.0,"counterVol":0,"realtimeFlow":0,"setPressPaddle":0.0}**/ + const parsedJSON = JSON.parse(response.data); + const temp = parsedJSON.tempBoilerCoffe; + const press = parsedJSON.pumpPress * 10; + + this.temperature = temp; + this.pressure = press; + if (_callback) { + _callback(); + } + } catch (e) {} + }, + (response) => { + // prints 403 + } + ); + } + + public startShot() { + const promise = new Promise((resolve, reject) => { + const options = { + method: 'get', + }; + + let urlAdding = '/api/action/man'; + + cordova.plugin.http.sendRequest( + this.getPreparation().connectedPreparationDevice.url + urlAdding, + options, + (response) => { + try { + const parsedJSON = JSON.parse(response.data); + resolve(parsedJSON); + } catch (e) { + reject(JSON.stringify(e)); + } + }, + (response) => { + // prints 403 + reject(JSON.stringify(response)); + } + ); + }); + return promise; + } + public stopShot() { + const promise = new Promise((resolve, reject) => { + const options = { + method: 'get', + }; + + let urlAdding = '/api/action/man'; + + cordova.plugin.http.sendRequest( + this.getPreparation().connectedPreparationDevice.url + urlAdding, + options, + (response) => { + try { + const parsedJSON = JSON.parse(response.data); + resolve(parsedJSON); + } catch (e) { + reject(JSON.stringify(e)); + } + }, + (response) => { + // prints 403 + reject(JSON.stringify(response)); + } + ); + }); + return promise; + } +} + +export class SanremoYOUParams implements ISanremoYOUParams { + public stopAtWeight: number = 0; + public residualLagTime: number = 0.9; + public selectedMode: SanremoYOUMode = SanremoYOUMode.LISTENING; + constructor() { + this.residualLagTime = 0.9; + this.selectedMode = SanremoYOUMode.LISTENING; + } +} diff --git a/src/classes/preparationDevice/xenia/xeniaDevice.ts b/src/classes/preparationDevice/xenia/xeniaDevice.ts index ef5d9578e..2b699b24a 100644 --- a/src/classes/preparationDevice/xenia/xeniaDevice.ts +++ b/src/classes/preparationDevice/xenia/xeniaDevice.ts @@ -337,6 +337,7 @@ export class XeniaParams implements IXeniaParams { public scriptAtFirstDripId: number = 0; public scriptAtWeightReachedId: number = 0; public scriptAtWeightReachedNumber: number = 0; + public residualLagTime: number = 0; constructor() {} } diff --git a/src/classes/settings/settings.ts b/src/classes/settings/settings.ts index fdb5c42c4..1adf7cb10 100755 --- a/src/classes/settings/settings.ts +++ b/src/classes/settings/settings.ts @@ -88,6 +88,15 @@ export class Settings implements ISettings { ARCHIVED: IBeanPageSort; FROZEN: IBeanPageSort; }; + public bean_collapsed: { + OPEN: boolean; + ARCHIVED: boolean; + FROZEN: boolean; + }; + public brew_collapsed: { + OPEN: boolean; + ARCHIVED: boolean; + }; public green_bean_sort: { OPEN: IBeanPageSort; @@ -131,6 +140,11 @@ export class Settings implements ISettings { }; }; + public graph_pressure: { + upper: number; + lower: number; + }; + public wake_lock: boolean; public security_check_when_going_back: boolean; @@ -317,6 +331,15 @@ export class Settings implements ISettings { FROZEN: {} as IBeanPageSort, }; + this.bean_collapsed = { + OPEN: false, + ARCHIVED: false, + FROZEN: false, + }; + this.brew_collapsed = { + OPEN: false, + ARCHIVED: false, + }; this.green_bean_sort = { OPEN: {} as IBeanPageSort, ARCHIVED: {} as IBeanPageSort, @@ -363,6 +386,11 @@ export class Settings implements ISettings { }, }; + this.graph_pressure = { + lower: 0, + upper: 9, + }; + this.brew_rating = 5; this.brew_rating_steps = 1; this.bean_rating = 5; @@ -505,14 +533,7 @@ export class Settings implements ISettings { ); } - public resetFilter() { - this.brew_filter = { - OPEN: {} as IBrewPageFilter, - ARCHIVED: {} as IBrewPageFilter, - }; - this.brew_filter.OPEN = this.GET_BREW_FILTER(); - this.brew_filter.ARCHIVED = this.GET_BREW_FILTER(); - + public resetBeanFilter() { this.bean_filter = { OPEN: {} as IBeanPageFilter, ARCHIVED: {} as IBeanPageFilter, @@ -521,7 +542,23 @@ export class Settings implements ISettings { this.bean_filter.OPEN = this.GET_BEAN_FILTER(); this.bean_filter.ARCHIVED = this.GET_BEAN_FILTER(); this.bean_filter.FROZEN = this.GET_BEAN_FILTER(); + } + + public resetBrewFilter() { + this.brew_filter = { + OPEN: {} as IBrewPageFilter, + ARCHIVED: {} as IBrewPageFilter, + }; + this.brew_filter.OPEN = this.GET_BREW_FILTER(); + this.brew_filter.ARCHIVED = this.GET_BREW_FILTER(); + } + + public resetFilter() { + this.resetBrewFilter(); + this.resetBeanFilter(); + } + public resetBeanSort() { this.bean_sort = { OPEN: {} as IBeanPageSort, ARCHIVED: {} as IBeanPageSort, diff --git a/src/components/bean-information/bean-information.component.html b/src/components/bean-information/bean-information.component.html index a4831a2f8..8b4a5cd3e 100644 --- a/src/components/bean-information/bean-information.component.html +++ b/src/components/bean-information/bean-information.component.html @@ -43,14 +43,15 @@ - +
+
- + @@ -144,7 +145,7 @@ {{"BEANS_AMOUNT_USED" | translate}}
- {{daysOld()}}
diff --git a/src/components/bean-information/bean-information.component.ts b/src/components/bean-information/bean-information.component.ts index ecc2130fa..3f3617cd3 100644 --- a/src/components/bean-information/bean-information.component.ts +++ b/src/components/bean-information/bean-information.component.ts @@ -51,7 +51,7 @@ export class BeanInformationComponent implements OnInit { @Input() public bean: Bean; @Input() public showActions: boolean = true; @Input() public disabled: boolean = false; - + @Input() public collapsed: boolean = false; @ViewChild('card', { read: ElementRef }) public cardEl: ElementRef; @ViewChild('beanStars', { read: NgxStarsComponent, static: false }) @@ -498,7 +498,7 @@ export class BeanInformationComponent implements OnInit { private async resetSettings() { const settings: Settings = this.uiSettingsStorage.getSettings(); - settings.resetFilter(); + settings.resetBeanFilter(); await this.uiSettingsStorage.saveSettings(settings); } diff --git a/src/components/beans/bean-freeze-information/bean-freeze-information.component.html b/src/components/beans/bean-freeze-information/bean-freeze-information.component.html index 4238b14a4..175b0079f 100644 --- a/src/components/beans/bean-freeze-information/bean-freeze-information.component.html +++ b/src/components/beans/bean-freeze-information/bean-freeze-information.component.html @@ -1,10 +1,10 @@ - + - + - - - - - - - - -
- -
-
- -
- -
-
- - - - - {{brew.config.unix_timestamp | formatDate:[settings?.date_format]}}{{brew.config.unix_timestamp | formatDate:["HH:mm"]}} +
+ + + + + + + + - - - - + +
+ +
-
- -
{{bean?.name}} ({{bean?.roaster}})
- - - + +
+ +
- + + + + + + +
+ + +
+ + () + + {{ bean?.name }} ({{ bean?.roaster }}) +
+
+ + + + + + {{ this.uiHelper.toFixedIfNecessary(brew.rating, 2) }} + +
+
+ + +
+ + +
- - - - - -
-
-
- - - - + + + {{ "BREW_DATA_PREPARATION_METHOD" | translate }} +
+ {{ preparation?.name }} +
+ - - {{brew?.grind_weight | number : '.0-2'}}gr / {{brew.brew_beverage_quantity | number : '.0-2'}}{{brewQuantityEnum[brew?.brew_beverage_quantity_type]}} ({{brew?.getBrewRatio()}}) - - - +
+ {{ mill?.name }} +
+ + {{ "BREW_DATA_IN_OUT_BR" | translate }} +
+ + {{ brew?.grind_weight | number : '.0-2' }}gr + / {{ brew.brew_quantity | number : '.0-2' }}{{ brewQuantityEnum[brew?.brew_quantity_type] }} ({{ brew?.getBrewRatio() }} + ) + + + {{ brew?.grind_weight | number : '.0-2' }}gr + / {{ brew.brew_beverage_quantity | number : '.0-2' }}{{ brewQuantityEnum[brew?.brew_beverage_quantity_type] }} ({{ brew?.getBrewRatio() }} + ) + +
+
- {{brew?.getFormattedTotalCoffeeBrewTime()}} -
- 0 && uiBrewHelper.fieldVisible(settings.visible_list_view_parameters.mill_speed, preparation?.visible_list_view_parameters.mill_speed, - preparation?.use_custom_parameters))" size="6"> - {{'BREW_DATA_GRIND_SIZE' | translate}}
- + {{ 'BREW_DATA_GRIND_SIZE' | translate }} +
+ {{brew?.grind_size}}{{ brew?.grind_size }}
- {{brew?.tds}}, %{{brew?.getExtractionYield()}} -
- - {{'BREW_DATA_BREW_TEMPERATURE' | translate}}
- {{brew?.brew_temperature}} -
- - {{'BREW_DATA_PRESSURE_PROFILE' | translate}}
- {{brew?.pressure_profile}} -
- - {{"BREW_DATA_BEAN_WEIGHT_IN" | translate}}
- {{brew?.bean_weight_in}} -
- +
+ {{ brew?.bean_weight_in }} +
+ + {{ "BREW_DATA_MILL_TIMER" | translate }} +
+ {{ brew?.mill_timer }} +
+ - {{"BREW_DATA_VESSEL_NAME_WEIGHT" | translate}}
- {{brew?.vessel_name}} / {{brew?.vessel_weight}} -
- +
+ {{ brew?.vessel_name }} / {{ brew?.vessel_weight }} +
+ + {{ "BREW_DATA_TEMPERATURE_TIME" | translate }} +
+ {{ brew?.getFormattedTotalCoffeeTemperatureTime() }} +
+ + {{ "BREW_DATA_COFFEE_BLOOMING_TIME" | translate }} +
+ {{ brew?.getFormattedTotalCoffeeBloomingTime() }} +
+ 0)'> + {{ "BREW_DATA_COFFEE_FIRST_DRIP_TIME" | translate }} +
+ {{ brew?.getFormattedTotalCoffeeFirstDripTime() }} +
+ - {{"BREW_DATA_WATER" | translate}}
- {{brew?.getWater().name}} -
- +
+ {{ brew?.getWater().name }} +
+ - {{"BREW_DATA_COFFEE_TYPE" | translate}}
- {{brew?.coffee_type}} -
- +
+ {{ brew?.coffee_type }} +
+ - {{"BREW_DATA_COFFEE_CONCENTRATION" | translate}}
- {{brew?.coffee_concentration}} -
- +
+ {{ brew?.coffee_concentration }} +
+ + {{ "BREW_INFORMATION_BEAN_AGE" | translate }} +
+ {{ brew?.getCalculatedBeanAge() }} +
+
- {{getCuppedBrewFlavors()}} -
- +
+ {{ brew?.getPreparationToolName(uuid) }} + +
+
+ + {{ 'BREW_DATA_FLAVOR' | translate }} +
+ {{ getCuppedBrewFlavors() }} +
+ - {{"BREW_DATA_NOTES" | translate}}
-
{{brew?.note}}
-
+ {{ "BREW_DATA_NOTES" | translate }} +
+ +
{{ brew?.note }}
+
+
-
-
-
-
-
+ + + + +
+ + + + + + + + + +
+ +
- - - - - + + + + + -
-
- {{brew.config.unix_timestamp | formatDate:["DD"]}}.
- - {{brew.config.unix_timestamp | formatDate:["MMMM"]}}
{{brew.config.unix_timestamp | formatDate:["YYYY"]}} +
+
+ {{ brew.config.unix_timestamp | formatDate:["DD"] }}.
+ + {{ brew.config.unix_timestamp | formatDate:["MMMM"] }} +
{{ brew.config.unix_timestamp | formatDate:["YYYY"] }}
-
+
-
-
- - -
- - - - - - - {{bean?.name}} - - - - {{preparation?.name}} - - - - - {{mill?.name}} - - - - - - - - - - - - - -
-
- - +
+
+
+ +
+ + + + + + + + + + + () + + {{ bean?.name }} + + + + + {{ preparation?.name }} + + + + + {{ mill?.name }} + + + + + + + + + + + + + + +
+
+
+
diff --git a/src/components/brew-information/brew-information.component.scss b/src/components/brew-information/brew-information.component.scss index 655feba5f..6ec6155ba 100644 --- a/src/components/brew-information/brew-information.component.scss +++ b/src/components/brew-information/brew-information.component.scss @@ -9,6 +9,7 @@ max-height:60px!important; } ion-card.brew-layout { + width:100%; margin-top:0px; margin-bottom:0px; margin-left:10px; @@ -54,6 +55,7 @@ top: -5px; } } + ion-card.dashboard-layout { @@ -143,6 +145,22 @@ max-height: 60px; object-fit:cover; } + + swiper-slide { + display: flex; + position: relative; + + width: 100%; + + height: auto; + } + + .add-bottom-spacing{ + margin-bottom:20px; + } + + + } diff --git a/src/components/brew-information/brew-information.component.ts b/src/components/brew-information/brew-information.component.ts index e333a98f6..3537da569 100644 --- a/src/components/brew-information/brew-information.component.ts +++ b/src/components/brew-information/brew-information.component.ts @@ -10,7 +10,7 @@ import { } from '@angular/core'; import { Brew } from '../../classes/brew/brew'; import { UISettingsStorage } from '../../services/uiSettingsStorage'; -import { ModalController, Platform } from '@ionic/angular'; +import { MenuController, ModalController, Platform } from '@ionic/angular'; import { BREW_ACTION } from '../../enums/brews/brewAction'; import { BrewPopoverActionsComponent } from '../../app/brew/brew-popover-actions/brew-popover-actions.component'; import { Bean } from '../../classes/bean/bean'; @@ -33,13 +33,13 @@ import { TranslateService } from '@ngx-translate/core'; import { BrewTrackingService } from '../../services/brewTracking/brew-tracking.service'; import { UIHealthKit } from '../../services/uiHealthKit'; import * as htmlToImage from 'html-to-image'; -import { Visualizer } from '../../classes/visualizer/visualizer'; import { UIFileHelper } from '../../services/uiFileHelper'; import { BrewFlow } from '../../classes/brew/brewFlow'; -import { UIBeanStorage } from '../../services/uiBeanStorage'; + import { UIBeanHelper } from '../../services/uiBeanHelper'; import { VisualizerService } from '../../services/visualizerService/visualizer-service.service'; +import { UIGraphHelper } from '../../services/uiGraphHelper'; declare var window; @Component({ selector: 'brew-information', @@ -48,9 +48,24 @@ declare var window; }) export class BrewInformationComponent implements OnInit { @Input() public brew: Brew; + @Input() public collapsed: boolean = false; @Input() public layout: string = 'brew'; @ViewChild('card', { read: ElementRef }) public cardEl: ElementRef; + + public slideOpts = { + allowTouchMove: false, + speed: 400, + slide: 4, + }; + + @ViewChild('swiper', { static: false }) public brewInformationSlider: + | ElementRef + | undefined; + + @ViewChild('brewInformationContainer', { read: ElementRef, static: false }) + public brewInformationContainer: ElementRef; + @ViewChild('brewStars', { read: NgxStarsComponent, static: false }) public brewStars: NgxStarsComponent; @@ -64,6 +79,9 @@ export class BrewInformationComponent implements OnInit { public settings: Settings = null; + public informationContainerHeight: number = undefined; + public informationContainerWidth: number = undefined; + constructor( private readonly uiSettingsStorage: UISettingsStorage, public readonly uiBrewHelper: UIBrewHelper, @@ -80,9 +98,10 @@ export class BrewInformationComponent implements OnInit { private readonly uiHealthKit: UIHealthKit, private readonly platform: Platform, private readonly uiFileHelper: UIFileHelper, - private readonly uiBeanStorage: UIBeanStorage, private readonly uiBeanHelper: UIBeanHelper, - private readonly visualizerService: VisualizerService + private readonly visualizerService: VisualizerService, + private readonly uiGraphHelper: UIGraphHelper, + private readonly menu: MenuController ) {} public ngOnInit() { @@ -91,6 +110,25 @@ export class BrewInformationComponent implements OnInit { this.bean = this.brew.getBean(); this.preparation = this.brew.getPreparation(); this.mill = this.brew.getMill(); + + setTimeout(() => { + /**We calculcate the information here, to avoid expression-changed in angular, because it always triggered while scrolling cause of calucation functions**/ + this.informationContainerHeight = + this.brewInformationContainer?.nativeElement?.offsetHeight - 50; + this.informationContainerWidth = + this.brewInformationContainer?.nativeElement?.offsetWidth - 50; + + /**If we slide on a bigger tablet, somehow ionic triggering the menu when sliding from right to left, thats why we need to attach us to touchstart/end and to ignore the slide...**/ + this.brewInformationSlider?.nativeElement.swiper.on( + 'touchStart', + () => { + this.menu.swipeGesture(false); + } + ); + this.brewInformationSlider?.nativeElement.swiper.on('touchEnd', () => { + this.menu.swipeGesture(true); + }); + }, 150); } } @@ -129,6 +167,10 @@ export class BrewInformationComponent implements OnInit { this.brewAction.emit([BREW_ACTION.DETAIL, this.brew]); } + public async showBrewGraph() { + await this.uiGraphHelper.detailBrewGraph(this.brew); + } + public async showBrewActions(event): Promise { event.stopPropagation(); event.stopImmediatePropagation(); @@ -354,6 +396,9 @@ export class BrewInformationComponent implements OnInit { case BREW_ACTION.SHOW_VISUALIZER: await this.showVisualizerShot(); break; + case BREW_ACTION.SHOW_GRAPH: + await this.showBrewGraph(); + break; default: break; } @@ -400,12 +445,12 @@ export class BrewInformationComponent implements OnInit { await this.uiAlert.showLoadingSpinner(); if (this.platform.is('ios')) { htmlToImage - .toJpeg(this.cardEl.nativeElement) + .toPng(this.cardEl.nativeElement) .then((_dataURL) => { // On iOS we need to do this a second time, because the rendering doesn't render everything (strange thing) setTimeout(() => { htmlToImage - .toJpeg(this.cardEl.nativeElement) + .toPng(this.cardEl.nativeElement) .then(async (_dataURLSecond) => { await this.uiAlert.hideLoadingSpinner(); setTimeout(() => { @@ -422,7 +467,7 @@ export class BrewInformationComponent implements OnInit { }); } else { htmlToImage - .toJpeg(this.cardEl.nativeElement) + .toPng(this.cardEl.nativeElement) .then(async (_dataURL) => { await this.uiAlert.hideLoadingSpinner(); setTimeout(() => { diff --git a/src/components/brews/brew-brewing-graph/brew-brewing-graph.component.ts b/src/components/brews/brew-brewing-graph/brew-brewing-graph.component.ts index 7aa9f234f..74e5bcfe5 100644 --- a/src/components/brews/brew-brewing-graph/brew-brewing-graph.component.ts +++ b/src/components/brews/brew-brewing-graph/brew-brewing-graph.component.ts @@ -62,6 +62,9 @@ import { Graph } from '../../../classes/graph/graph'; import { UIGraphStorage } from '../../../services/uiGraphStorage.service'; import regression from 'regression'; import { TextToSpeechService } from '../../../services/textToSpeech/text-to-speech.service'; +import { SanremoYOUDevice } from '../../../classes/preparationDevice/sanremo/sanremoYOUDevice'; +import { SanremoYOUMode } from '../../../enums/preparationDevice/sanremo/sanremoYOUMode'; +import { BookooScale } from '../../../classes/devices/bokooScale'; declare var Plotly; @@ -133,6 +136,7 @@ export class BrewBrewingGraphComponent implements OnInit { private temperatureThresholdWasHit: boolean = false; private xeniaOverviewInterval: any = undefined; private meticulousInterval: any = undefined; + private sanremoYOUFetchingInterval: any = undefined; public lastChartLayout: any = undefined; public lastChartRenderingInstance: number = 0; @@ -153,6 +157,7 @@ export class BrewBrewingGraphComponent implements OnInit { public textToSpeechWeightInterval: any = undefined; public textToSpeechTimerInterval: any = undefined; + constructor( private readonly platform: Platform, private readonly bleManager: CoffeeBluetoothDevicesService, @@ -925,6 +930,7 @@ export class BrewBrewingGraphComponent implements OnInit { if (this.isDetail === false) { let graph_weight_settings; let graph_flow_settings; + if ( this.data.getPreparation().style_type === PREPARATION_STYLE_TYPE.ESPRESSO @@ -1033,6 +1039,9 @@ export class BrewBrewingGraphComponent implements OnInit { this.brewComponent?.brewBrewingPreparationDeviceEl?.preparationDeviceConnected() || !this.platform.is('cordova') ) { + const graph_pressure_settings = this.settings.graph_pressure; + const suggestedMinPressure: number = graph_pressure_settings.lower; + const suggestedMaxPressure: number = graph_pressure_settings.upper; layout['yaxis4'] = { title: '', titlefont: { color: '#05C793' }, @@ -1043,7 +1052,7 @@ export class BrewBrewingGraphComponent implements OnInit { showgrid: false, position: 0.91, fixedrange: true, - range: [0, 10], + range: [suggestedMinPressure, suggestedMaxPressure], }; } const temperatureDevice = this.bleManager.getTemperatureDevice(); @@ -1105,6 +1114,16 @@ export class BrewBrewingGraphComponent implements OnInit { }, }; + const graph_pressure_settings = this.settings.graph_pressure; + const suggestedMinPressure = graph_pressure_settings.lower; + let suggestedMaxPressure = graph_pressure_settings.upper; + try { + if (this.pressureTrace?.y.length > 0) { + suggestedMaxPressure = Math.max(...this.pressureTrace.y); + suggestedMaxPressure = Math.ceil(suggestedMaxPressure + 1); + } + } catch (ex) {} + layout['yaxis4'] = { title: '', titlefont: { color: '#05C793' }, @@ -1114,7 +1133,7 @@ export class BrewBrewingGraphComponent implements OnInit { side: 'right', showgrid: false, position: 0.93, - range: [0, 12], + range: [suggestedMinPressure, suggestedMaxPressure], visible: true, }; @@ -1577,6 +1596,18 @@ export class BrewBrewingGraphComponent implements OnInit { newLayoutIsNeeded = true; } } + if (this.pressureTrace?.y?.length > 0) { + // #783 + const lastPressureData: number = + this.pressureTrace.y[this.pressureTrace.y.length - 1]; + if (lastPressureData > this.lastChartLayout.yaxis4.range[1]) { + this.lastChartLayout.yaxis4.range[1] = Math.ceil( + lastPressureData + 1 + ); + newLayoutIsNeeded = true; + } + } + if (newLayoutIsNeeded) { Plotly.relayout( this.profileDiv.nativeElement, @@ -1585,18 +1616,7 @@ export class BrewBrewingGraphComponent implements OnInit { } }, 25); } else { - const delay = moment(new Date()) - .startOf('day') - .add('seconds', 0) - .toDate() - .getTime(); - const delayedTime: number = moment(new Date()) - .startOf('day') - .add('seconds', this.brewComponent.timer.getSeconds() + 5) - .toDate() - .getTime(); - this.lastChartLayout.xaxis.range = [delay, delayedTime]; - Plotly.relayout(this.profileDiv.nativeElement, this.lastChartLayout); + // Not needed anymore } } catch (ex) {} }); @@ -1753,6 +1773,7 @@ export class BrewBrewingGraphComponent implements OnInit { PreparationDeviceType.XENIA && this.machineStopScriptWasTriggered === false ) { + this.machineStopScriptWasTriggered = true; // If the event is not xenia, we pressed buttons, if the event was triggerd by xenia, timer already stopped. // If we press pause, stop scripts. this.uiLog.log(`Xenia Script - Pause button pressed, stop script`); @@ -1774,6 +1795,29 @@ export class BrewBrewingGraphComponent implements OnInit { ); this.stopFetchingAndSettingDataFromXenia(); } + if ( + this.brewComponent?.brewBrewingPreparationDeviceEl?.preparationDeviceConnected() && + this.brewComponent?.brewBrewingPreparationDeviceEl?.getPreparationDeviceType() === + PreparationDeviceType.SANREMO_YOU && + _event !== 'sanremo_you' && + this.machineStopScriptWasTriggered === false && + this.data.preparationDeviceBrew.params.selectedMode === + SanremoYOUMode.CONTROLLING + ) { + this.machineStopScriptWasTriggered = true; + this.uiLog.log(`Sanremo YOU - Pause button pressed, stop shot`); + const prepDeviceCall: SanremoYOUDevice = this.brewComponent + ?.brewBrewingPreparationDeviceEl?.preparationDevice as SanremoYOUDevice; + prepDeviceCall.stopShot().catch((_msg) => { + this.uiToast.showInfoToast( + 'We could not stop - manual triggered: ' + _msg, + false + ); + this.uiLog.log('We could not stop - manual triggered: ' + _msg); + }); + this.stopFetchingDataFromSanremoYOU(); + } + if ( this.brewComponent?.brewBrewingPreparationDeviceEl?.preparationDeviceConnected() && this.brewComponent?.brewBrewingPreparationDeviceEl?.getPreparationDeviceType() === @@ -1826,6 +1870,33 @@ export class BrewBrewingGraphComponent implements OnInit { } } + public startFetchingDataFromSanremoYOU() { + const prepDeviceCall: SanremoYOUDevice = this.brewComponent + .brewBrewingPreparationDeviceEl.preparationDevice as SanremoYOUDevice; + + this.stopFetchingDataFromSanremoYOU(); + const setSanremoData = () => { + const temp = prepDeviceCall.getTemperature(); + const press = prepDeviceCall.getPressure(); + this.__setPressureFlow({ actual: press, old: press }); + this.__setTemperatureFlow({ actual: temp, old: temp }); + }; + prepDeviceCall.fetchRuntimeData(() => { + // before we start the interval, we fetch the data once to overwrite, and set them. + setSanremoData(); + }); + + this.sanremoYOUFetchingInterval = setInterval(async () => { + try { + // We don't use the callback function to make sure we don't have to many performance issues + prepDeviceCall.fetchRuntimeData(() => { + //before we start the interval, we fetch the data once to overwrite, and set them. + setSanremoData(); + }); + } catch (ex) {} + }, 100); + } + public startFetchingDataFromMeticulous() { const prepDeviceCall: MeticulousDevice = this.brewComponent .brewBrewingPreparationDeviceEl.preparationDevice as MeticulousDevice; @@ -1961,6 +2032,13 @@ export class BrewBrewingGraphComponent implements OnInit { } } + public stopFetchingDataFromSanremoYOU() { + if (this.sanremoYOUFetchingInterval !== undefined) { + clearInterval(this.sanremoYOUFetchingInterval); + this.sanremoYOUFetchingInterval = undefined; + } + } + public stopFetchingDataFromMeticulous() { if (this.meticulousInterval !== undefined) { clearInterval(this.meticulousInterval); @@ -2065,7 +2143,7 @@ export class BrewBrewingGraphComponent implements OnInit { ); weight = weight + genRand(0.1, 2, 2); pressure = Math.floor( - (crypto.getRandomValues(new Uint8Array(1))[0] / Math.pow(2, 8)) * 11 + (crypto.getRandomValues(new Uint8Array(1))[0] / Math.pow(2, 8)) * 16 ); temperature = Math.floor( (crypto.getRandomValues(new Uint8Array(1))[0] / Math.pow(2, 8)) * 90 @@ -2225,36 +2303,60 @@ export class BrewBrewingGraphComponent implements OnInit { this.brewComponent?.brewBrewingPreparationDeviceEl?.getPreparationDeviceType() === PreparationDeviceType.METICULOUS ) { + const prepDeviceCall: MeticulousDevice = this.brewComponent + .brewBrewingPreparationDeviceEl.preparationDevice as MeticulousDevice; + + if (this.data.preparationDeviceBrew.params.chosenProfileId !== '') { + this.uiLog.log( + `A Meticulous profile was choosen, execute it - ${this.data.preparationDeviceBrew.params.chosenProfileId}` + ); + await prepDeviceCall.loadProfileByID( + this.data.preparationDeviceBrew.params.chosenProfileId + ); + await prepDeviceCall.startExecute(); + } else { + this.uiLog.log( + 'No Meticulous profile was selected, just listen for the start' + ); + } + if ( - this.brewComponent?.brewBrewingPreparationDeviceEl?.getPreparationDeviceType() === - PreparationDeviceType.METICULOUS + this.settings.bluetooth_scale_maximize_on_start_timer === true && + this.brewComponent.maximizeFlowGraphIsShown === false ) { - const prepDeviceCall: MeticulousDevice = this.brewComponent - .brewBrewingPreparationDeviceEl.preparationDevice as MeticulousDevice; + this.brewComponent.maximizeFlowGraph(); + } - if (this.data.preparationDeviceBrew.params.chosenProfileId !== '') { - this.uiLog.log( - `A Meticulous profile was choosen, execute it - ${this.data.preparationDeviceBrew.params.chosenProfileId}` - ); - await prepDeviceCall.loadProfileByID( - this.data.preparationDeviceBrew.params.chosenProfileId - ); - await prepDeviceCall.startExecute(); - } else { - this.uiLog.log( - 'No Meticulous profile was selected, just listen for the start' - ); - } + this.startFetchingDataFromMeticulous(); + } else if ( + this.brewComponent?.brewBrewingPreparationDeviceEl?.preparationDeviceConnected() && + this.brewComponent?.brewBrewingPreparationDeviceEl?.getPreparationDeviceType() === + PreparationDeviceType.SANREMO_YOU + ) { + const prepDeviceCall: SanremoYOUDevice = this.brewComponent + .brewBrewingPreparationDeviceEl.preparationDevice as SanremoYOUDevice; - if ( - this.settings.bluetooth_scale_maximize_on_start_timer === true && - this.brewComponent.maximizeFlowGraphIsShown === false - ) { - this.brewComponent.maximizeFlowGraph(); - } + if ( + this.data.preparationDeviceBrew?.params.selectedMode === + SanremoYOUMode.CONTROLLING + ) { + prepDeviceCall.startShot().catch((_msg) => { + this.uiLog.log('We could not start shot on sanremo: ' + _msg); + this.uiToast.showInfoToast( + 'We could not start shot on sanremo: ' + _msg, + false + ); + }); + } - this.startFetchingDataFromMeticulous(); + if ( + this.settings.bluetooth_scale_maximize_on_start_timer === true && + this.brewComponent.maximizeFlowGraphIsShown === false + ) { + this.brewComponent.maximizeFlowGraph(); } + + this.startFetchingDataFromSanremoYOU(); } } @@ -2292,6 +2394,11 @@ export class BrewBrewingGraphComponent implements OnInit { await this.timerReset(undefined); await this.brewComponent.timer.resetWithoutEmit(false); + if (scale.getScaleType() === ScaleType.BOKOOSCALE) { + //const bookooScale: BookooScale = scale as BookooScale; + //await bookooScale.tareAndStartTimerModeAuto(); + } + this.brewComponent.timer.checkChanges(); this.checkChanges(); @@ -2656,8 +2763,9 @@ export class BrewBrewingGraphComponent implements OnInit { * We try to match also turbo-shots which are like 7-8 grams. * Scales with just 3 values per second would be like 7 / 3 values per second = 2.33g increase each tick. * So we won't get jump from like 1 to 10 gram, then to like 40 grams + * Update 26.08.24 - We change from 5 to 6, because we had one shot where the value jumped from 0 to 5,5 and we didn't track anymore */ - const plausibleEspressoWeightIncreaseBound: number = 5; + const plausibleEspressoWeightIncreaseBound: number = 6; risingFactorOK = entryBeforeVal + plausibleEspressoWeightIncreaseBound >= weight; @@ -2691,6 +2799,164 @@ export class BrewBrewingGraphComponent implements OnInit { }; } + private calculateBrewByWeight( + _currentWeightValue: number, + _residualLagTime: number, + _targetWeight: number, + _brewByWeightActive: boolean, + _scale: BluetoothScale + ) { + let weight: number = this.uiHelper.toFixedIfNecessary( + _currentWeightValue, + 1 + ); + if (this.ignoreScaleWeight === true) { + if (this.flowProfileTempAll.length > 0) { + const oldFlowProfileTemp = + this.flowProfileTempAll[this.flowProfileTempAll.length - 1]; + weight = this.uiHelper.toFixedIfNecessary(oldFlowProfileTemp.weight, 1); + } + } + + if ( + this.flow_profile_raw.realtimeFlow && + this.flow_profile_raw.realtimeFlow.length > 0 + ) { + const targetWeight = _targetWeight; + + const brewByWeightActive: boolean = _brewByWeightActive; + + let n = 3; + if (this.flowNCalculation > 0) { + n = this.flowNCalculation; + } else { + n = this.flowProfileTempAll.length; + } + const lag_time = this.uiHelper.toFixedIfNecessary(1 / n, 2); + + let average_flow_rate = 0; + let lastFlowValue = 0; + + const linearArray = []; + + const weightFlowCalc: Array = + this.flow_profile_raw.weight.slice(-(n - 1)); + + for (let i = 0; i < weightFlowCalc.length; i++) { + if (weightFlowCalc[i] && weightFlowCalc[i].actual_weight) { + const linearArrayEntry = [i, weightFlowCalc[i].actual_weight]; + linearArray.push(linearArrayEntry); + } + } + linearArray.push([n - 1, weight]); + + const linearRegressionCalc = regression.linear(linearArray); + average_flow_rate = linearRegressionCalc.equation[0] * n; + + const scaleType = _scale.getScaleType(); + + this.pushBrewByWeight( + targetWeight, + lag_time, + this.flowTime + '.' + this.flowSecondTick, + lastFlowValue, + weight, + lag_time + _residualLagTime, + weight + average_flow_rate * (lag_time + _residualLagTime) >= + targetWeight, + average_flow_rate * (lag_time + _residualLagTime), + _residualLagTime, + average_flow_rate, + scaleType + ); + let thresholdHit: boolean = false; + if (brewByWeightActive) { + thresholdHit = + weight + average_flow_rate * (lag_time + _residualLagTime) >= + targetWeight; + } else { + thresholdHit = weight >= targetWeight; + } + return thresholdHit; + } + return false; + } + + private triggerStopShotOnXenia(_actualScaleWeight) { + const prepDeviceCall: XeniaDevice = this.brewComponent + .brewBrewingPreparationDeviceEl.preparationDevice as XeniaDevice; + if (this.data.preparationDeviceBrew.params.scriptAtWeightReachedId > 0) { + this.uiLog.log( + `Xenia Script - Weight Reached: ${_actualScaleWeight} - Trigger custom script` + ); + prepDeviceCall + .startScript( + this.data.preparationDeviceBrew.params.scriptAtWeightReachedId + ) + .catch((_msg) => { + this.uiToast.showInfoToast( + 'We could not start script at weight: ' + _msg, + false + ); + this.uiLog.log('We could not start script at weight: ' + _msg); + }); + this.writeExecutionTimeToNotes( + 'Weight reached script', + this.data.preparationDeviceBrew.params.scriptAtWeightReachedId, + this.getScriptName( + this.data.preparationDeviceBrew.params.scriptAtWeightReachedId + ) + ); + } else { + this.uiLog.log(`Xenia Script - Weight Reached - Trigger stop script`); + prepDeviceCall.stopScript().catch((_msg) => { + this.uiToast.showInfoToast( + 'We could not stop script at weight: ' + _msg, + false + ); + this.uiLog.log('We could not stop script at weight: ' + _msg); + }); + this.writeExecutionTimeToNotes( + 'Stop script (Weight reached)', + 0, + this.translate.instant( + 'PREPARATION_DEVICE.TYPE_XENIA.SCRIPT_LIST_GENERAL_STOP' + ) + ); + } + + // This will be just called once, we stopped the shot and now we check if we directly shall stop or not + if ( + this.settings.bluetooth_scale_espresso_stop_on_no_weight_change === false + ) { + this.stopFetchingAndSettingDataFromXenia(); + this.brewComponent.timer.pauseTimer('xenia'); + } else { + // We weight for the normal "setFlow" to stop the detection of the graph, there then aswell is the stop fetch of the xenia triggered. + } + } + + private triggerStopShotOnSanremoYOU(_actualScaleWeight) { + const prepDeviceCall: SanremoYOUDevice = this.brewComponent + .brewBrewingPreparationDeviceEl.preparationDevice as SanremoYOUDevice; + + this.uiLog.log(`Sanremo YOU Stop: ${_actualScaleWeight}`); + prepDeviceCall.stopShot().catch((_msg) => { + this.uiToast.showInfoToast('We could not stop at weight: ' + _msg, false); + this.uiLog.log('We could not start script at weight: ' + _msg); + }); + + // This will be just called once, we stopped the shot and now we check if we directly shall stop or not + if ( + this.settings.bluetooth_scale_espresso_stop_on_no_weight_change === false + ) { + this.stopFetchingDataFromSanremoYOU(); + this.brewComponent.timer.pauseTimer('sanremo_you'); + } else { + // We weight for the normal "setFlow" to stop the detection of the graph, there then aswell is the stop fetch of the xenia triggered. + } + } + public attachToScaleWeightChange() { const scale: BluetoothScale = this.bleManager.getScale(); const preparationStyleType = this.data.getPreparation().style_type; @@ -2703,6 +2969,39 @@ export class BrewBrewingGraphComponent implements OnInit { } this.machineStopScriptWasTriggered = false; + + const prepDeviceConnected = + this.brewComponent.brewBrewingPreparationDeviceEl.preparationDeviceConnected(); + let residual_lag_time = 1.35; + let targetWeight = 0; + let brewByWeightActive: boolean = false; + let preparationDeviceType: PreparationDeviceType; + + if (prepDeviceConnected) { + preparationDeviceType = + this.brewComponent.brewBrewingPreparationDeviceEl.getPreparationDeviceType(); + switch (preparationDeviceType) { + case PreparationDeviceType.XENIA: + const prepXeniaDeviceCall: XeniaDevice = this.brewComponent + .brewBrewingPreparationDeviceEl.preparationDevice as XeniaDevice; + residual_lag_time = prepXeniaDeviceCall.getResidualLagTime(); + targetWeight = + this.data.preparationDeviceBrew.params + .scriptAtWeightReachedNumber; + brewByWeightActive = + this.data.preparationDeviceBrew?.params?.brew_by_weight_active; + break; + case PreparationDeviceType.SANREMO_YOU: + const prepSanremoDeviceCall: SanremoYOUDevice = this.brewComponent + .brewBrewingPreparationDeviceEl + .preparationDevice as SanremoYOUDevice; + residual_lag_time = prepSanremoDeviceCall.getResidualLagTime(); + targetWeight = this.data.preparationDeviceBrew.params.stopAtWeight; + brewByWeightActive = true; + break; + } + } + this.scaleFlowSubscription = scale.flowChange.subscribe((_valChange) => { let _val; if (this.ignoreScaleWeight === false) { @@ -2715,190 +3014,46 @@ export class BrewBrewingGraphComponent implements OnInit { _val = _valChange; } - if ( - this.brewComponent.timer.isTimerRunning() && - this.brewComponent.brewBrewingPreparationDeviceEl.preparationDeviceConnected() && - this.brewComponent.brewBrewingPreparationDeviceEl.getPreparationDeviceType() === - PreparationDeviceType.XENIA && - this.data.preparationDeviceBrew.params.scriptAtWeightReachedNumber > 0 - ) { - if (this.isFirstXeniaScriptSet()) { - let weight: number = this.uiHelper.toFixedIfNecessary( + if (this.brewComponent.timer.isTimerRunning() && prepDeviceConnected) { + if ( + preparationDeviceType === PreparationDeviceType.XENIA && + this.data.preparationDeviceBrew.params.scriptAtWeightReachedNumber > + 0 && + this.isFirstXeniaScriptSet() + ) { + /**We call this function before the if, because we still log the data**/ + const thresholdHit = this.calculateBrewByWeight( _val.actual, - 1 + residual_lag_time, + targetWeight, + brewByWeightActive, + scale ); - if (this.ignoreScaleWeight === true) { - if (this.flowProfileTempAll.length > 0) { - const oldFlowProfileTemp = - this.flowProfileTempAll[this.flowProfileTempAll.length - 1]; - weight = this.uiHelper.toFixedIfNecessary( - oldFlowProfileTemp.weight, - 1 - ); - } - } - if ( - this.flow_profile_raw.realtimeFlow && - this.flow_profile_raw.realtimeFlow.length > 0 - ) { - const prepDeviceCall: XeniaDevice = this.brewComponent - .brewBrewingPreparationDeviceEl - .preparationDevice as XeniaDevice; - - const targetWeight = - this.data.preparationDeviceBrew.params - .scriptAtWeightReachedNumber; - - const brewByWeightActive: boolean = - this.data.preparationDeviceBrew?.params?.brew_by_weight_active; - - let n = 3; - if (this.flowNCalculation > 0) { - n = this.flowNCalculation; - } else { - n = this.flowProfileTempAll.length; - } - const lag_time = this.uiHelper.toFixedIfNecessary(1 / n, 2); - const residual_lag_time = prepDeviceCall.getResidualLagTime(); - - let average_flow_rate = 0; - let lastFlowValue = 0; - - const linearArray = []; - - const weightFlowCalc: Array = - this.flow_profile_raw.weight.slice(-(n - 1)); - - for (let i = 0; i < weightFlowCalc.length; i++) { - if (weightFlowCalc[i] && weightFlowCalc[i].actual_weight) { - const linearArrayEntry = [i, weightFlowCalc[i].actual_weight]; - linearArray.push(linearArrayEntry); - } + if (this.machineStopScriptWasTriggered === false) { + if (thresholdHit) { + this.machineStopScriptWasTriggered = true; + this.triggerStopShotOnXenia(_val.actual); } - linearArray.push([n - 1, weight]); - - const linearRegressionCalc = regression.linear(linearArray); - average_flow_rate = linearRegressionCalc.equation[0] * n; - - /** Old calculcation - try { - lastFlowValue = - this.flow_profile_raw.realtimeFlow[ - this.flow_profile_raw.realtimeFlow.length - 1 - ].flow_value; - - const avgFlowValCalc: Array = - this.flow_profile_raw.realtimeFlow.slice(-n); - - for (let i = 0; i < avgFlowValCalc.length; i++) { - if (avgFlowValCalc[i] && avgFlowValCalc[i].flow_value) { - average_flow_rate = - average_flow_rate + avgFlowValCalc[i].flow_value; - } - } - if (average_flow_rate > 0) { - average_flow_rate = this.uiHelper.toFixedIfNecessary( - average_flow_rate / n, - 2 - ); - } - } catch (ex) {}**/ - - const scaleType = scale.getScaleType(); - - this.pushBrewByWeight( - this.data.preparationDeviceBrew.params - .scriptAtWeightReachedNumber, - lag_time, - this.flowTime + '.' + this.flowSecondTick, - lastFlowValue, - weight, - lag_time + residual_lag_time, - weight + average_flow_rate * (lag_time + residual_lag_time) >= - targetWeight, - average_flow_rate * (lag_time + residual_lag_time), - residual_lag_time, - average_flow_rate, - scaleType - ); - - if (this.machineStopScriptWasTriggered === false) { - let thresholdHit: boolean = false; - if (brewByWeightActive) { - thresholdHit = - weight + - average_flow_rate * (lag_time + residual_lag_time) >= - targetWeight; - } else { - thresholdHit = weight >= targetWeight; - } - - if (thresholdHit) { - if ( - this.data.preparationDeviceBrew.params - .scriptAtWeightReachedId > 0 - ) { - this.uiLog.log( - `Xenia Script - Weight Reached: ${weight} - Trigger custom script` - ); - prepDeviceCall - .startScript( - this.data.preparationDeviceBrew.params - .scriptAtWeightReachedId - ) - .catch((_msg) => { - this.uiToast.showInfoToast( - 'We could not start script at weight: ' + _msg, - false - ); - this.uiLog.log( - 'We could not start script at weight: ' + _msg - ); - }); - this.writeExecutionTimeToNotes( - 'Weight reached script', - this.data.preparationDeviceBrew.params - .scriptAtWeightReachedId, - this.getScriptName( - this.data.preparationDeviceBrew.params - .scriptAtWeightReachedId - ) - ); - } else { - this.uiLog.log( - `Xenia Script - Weight Reached - Trigger stop script` - ); - prepDeviceCall.stopScript().catch((_msg) => { - this.uiToast.showInfoToast( - 'We could not stop script at weight: ' + _msg, - false - ); - this.uiLog.log( - 'We could not stop script at weight: ' + _msg - ); - }); - this.writeExecutionTimeToNotes( - 'Stop script (Weight reached)', - 0, - this.translate.instant( - 'PREPARATION_DEVICE.TYPE_XENIA.SCRIPT_LIST_GENERAL_STOP' - ) - ); - } - this.machineStopScriptWasTriggered = true; - // This will be just called once, we stopped the shot and now we check if we directly shall stop or not - if ( - this.settings - .bluetooth_scale_espresso_stop_on_no_weight_change === - false - ) { - this.stopFetchingAndSettingDataFromXenia(); - this.brewComponent.timer.pauseTimer('xenia'); - } else { - // We weight for the normal "setFlow" to stop the detection of the graph, there then aswell is the stop fetch of the xenia triggered. - } - } + } + } else if ( + this.brewComponent.brewBrewingPreparationDeviceEl.getPreparationDeviceType() === + PreparationDeviceType.SANREMO_YOU && + this.data.preparationDeviceBrew.params.selectedMode === + SanremoYOUMode.CONTROLLING + ) { + /**We call this function before the if, because we still log the data**/ + const thresholdHit = this.calculateBrewByWeight( + _val.actual, + residual_lag_time, + targetWeight, + brewByWeightActive, + scale + ); + if (this.machineStopScriptWasTriggered === false) { + if (thresholdHit) { + this.machineStopScriptWasTriggered = true; + this.triggerStopShotOnSanremoYOU(_val.actual); } } } @@ -2940,6 +3095,7 @@ export class BrewBrewingGraphComponent implements OnInit { this.deattachToScaleStartTareListening(); this.stopFetchingAndSettingDataFromXenia(); this.stopFetchingDataFromMeticulous(); + this.stopFetchingDataFromSanremoYOU(); if (this.settings?.text_to_speech_active) { this.textToSpeech.end(); @@ -3514,6 +3670,13 @@ export class BrewBrewingGraphComponent implements OnInit { ) { this.stopFetchingAndSettingDataFromXenia(); } + if ( + this.brewComponent.brewBrewingPreparationDeviceEl.preparationDeviceConnected() && + this.brewComponent.brewBrewingPreparationDeviceEl.getPreparationDeviceType() === + PreparationDeviceType.SANREMO_YOU + ) { + this.stopFetchingDataFromSanremoYOU(); + } let isMeticulous: boolean = false; diff --git a/src/components/brews/brew-brewing-preparation-device/brew-brewing-preparation-device.component.html b/src/components/brews/brew-brewing-preparation-device/brew-brewing-preparation-device.component.html index 3e04427aa..0c923610d 100644 --- a/src/components/brews/brew-brewing-preparation-device/brew-brewing-preparation-device.component.html +++ b/src/components/brews/brew-brewing-preparation-device/brew-brewing-preparation-device.component.html @@ -73,6 +73,33 @@
+
+ + + {{"IMPORT_SHOT_FROM_METICULOUS" | translate}} + +
+ + + {{"PREPARATION_DEVICE.TYPE_SANREMO_YOU.TITLE" | translate }} + + + + + {{"PREPARATION_DEVICE.TYPE_SANREMO_YOU.MODE_LISTENING" | translate}} + + + {{"PREPARATION_DEVICE.TYPE_SANREMO_YOU.MODE_CONTROL" | translate}} + + + + + + + + diff --git a/src/components/brews/brew-brewing-preparation-device/brew-brewing-preparation-device.component.ts b/src/components/brews/brew-brewing-preparation-device/brew-brewing-preparation-device.component.ts index 34998a4d2..cb340e755 100644 --- a/src/components/brews/brew-brewing-preparation-device/brew-brewing-preparation-device.component.ts +++ b/src/components/brews/brew-brewing-preparation-device/brew-brewing-preparation-device.component.ts @@ -29,6 +29,22 @@ import { UISettingsStorage } from '../../../services/uiSettingsStorage'; import { Settings } from '../../../classes/settings/settings'; import { Preparation } from '../../../classes/preparation/preparation'; import { UIPreparationStorage } from '../../../services/uiPreparationStorage'; +import moment from 'moment'; +import { + BrewFlow, + IBrewPressureFlow, + IBrewRealtimeWaterFlow, + IBrewTemperatureFlow, + IBrewWeightFlow, +} from '../../../classes/brew/brewFlow'; +import { BrewModalImportShotMeticulousComponent } from '../../../app/brew/brew-modal-import-shot-meticulous/brew-modal-import-shot-meticulous.component'; +import { ModalController } from '@ionic/angular'; +import { HistoryListingEntry } from '@meticulous-home/espresso-api/dist/types'; +import { + SanremoYOUDevice, + SanremoYOUParams, +} from '../../../classes/preparationDevice/sanremo/sanremoYOUDevice'; +import { SanremoYOUMode } from '../../../enums/preparationDevice/sanremo/sanremoYOUMode'; @Component({ selector: 'brew-brewing-preparation-device', templateUrl: './brew-brewing-preparation-device.component.html', @@ -39,7 +55,8 @@ export class BrewBrewingPreparationDeviceComponent implements OnInit { @Input() public isEdit: boolean = false; @Output() public dataChange = new EventEmitter(); @Input() public brewComponent: BrewBrewingComponent; - public preparationDevice: XeniaDevice | MeticulousDevice = undefined; + public preparationDevice: XeniaDevice | MeticulousDevice | SanremoYOUDevice = + undefined; public preparation: Preparation = undefined; public settings: Settings = undefined; @@ -55,10 +72,11 @@ export class BrewBrewingPreparationDeviceComponent implements OnInit { private readonly uiBrewStorage: UIBrewStorage, private readonly uiHelper: UIHelper, private readonly uiToast: UIToast, - private readonly uiBrewHelper: UIBrewHelper, private readonly uiSettingsStorage: UISettingsStorage, private readonly uiPreparationStorage: UIPreparationStorage, - private readonly changeDetectorRef: ChangeDetectorRef + private readonly changeDetectorRef: ChangeDetectorRef, + private readonly modalController: ModalController, + public readonly uiBrewHelper: UIBrewHelper ) {} public ngOnInit() { @@ -95,6 +113,8 @@ export class BrewBrewingPreparationDeviceComponent implements OnInit { await this.instanceXeniaPreparationDevice(connectedDevice, _brew); } else if (connectedDevice instanceof MeticulousDevice) { await this.instanceMeticulousPreparationDevice(connectedDevice, _brew); + } else if (connectedDevice instanceof SanremoYOUDevice) { + await this.instanceSanremoYOUPreparationDevice(connectedDevice, _brew); } this.checkChanges(); } else { @@ -270,7 +290,7 @@ export class BrewBrewingPreparationDeviceComponent implements OnInit { this.data.preparationDeviceBrew.params = new MeticulousParams(); await connectedDevice.connectToSocket().then( - (_connected) => { + async (_connected) => { if (_connected) { this.preparationDevice = connectedDevice as MeticulousDevice; this.preparationDevice.loadProfiles(); @@ -282,11 +302,116 @@ export class BrewBrewingPreparationDeviceComponent implements OnInit { ); } + private async instanceSanremoYOUPreparationDevice( + connectedDevice: SanremoYOUDevice, + _brew: Brew = null + ) { + this.data.preparationDeviceBrew.type = PreparationDeviceType.SANREMO_YOU; + this.data.preparationDeviceBrew.params = new SanremoYOUParams(); + await connectedDevice.deviceConnected().then( + () => { + this.preparationDevice = connectedDevice as SanremoYOUDevice; + }, + () => { + //Not connected + } + ); + } + + public async importShotFromMeticulous() { + const modal = await this.modalController.create({ + component: BrewModalImportShotMeticulousComponent, + id: BrewModalImportShotMeticulousComponent.COMPONENT_ID, + componentProps: { + meticulousDevice: this.preparationDevice as MeticulousDevice, + }, + }); + + await modal.present(); + const rData = await modal.onWillDismiss(); + + if (rData && rData.data && rData.data.choosenHistory) { + const chosenEntry = rData.data.choosenHistory as HistoryListingEntry; + this.generateShotFlowProfileFromMeticulousData(chosenEntry); + } + } + + private generateShotFlowProfileFromMeticulousData(_historyData) { + const newMoment = moment(new Date()).startOf('day'); + + let firstDripTimeSet: boolean = false; + const newBrewFlow = new BrewFlow(); + + let seconds: number = 0; + let milliseconds: number = 0; + for (const entry of _historyData.data as any) { + const shotEntry: any = entry.shot; + const shotEntryTime = newMoment.clone().add('milliseconds', entry.time); + const timestamp = shotEntryTime.format('HH:mm:ss.SSS'); + + seconds = shotEntryTime.diff(newMoment, 'seconds'); + milliseconds = shotEntryTime.get('milliseconds'); + + const realtimeWaterFlow: IBrewRealtimeWaterFlow = + {} as IBrewRealtimeWaterFlow; + + realtimeWaterFlow.brew_time = ''; + realtimeWaterFlow.timestamp = timestamp; + realtimeWaterFlow.smoothed_weight = 0; + realtimeWaterFlow.flow_value = shotEntry.flow; + realtimeWaterFlow.timestampdelta = 0; + + newBrewFlow.realtimeFlow.push(realtimeWaterFlow); + + const brewFlow: IBrewWeightFlow = {} as IBrewWeightFlow; + brewFlow.timestamp = timestamp; + brewFlow.brew_time = ''; + brewFlow.actual_weight = shotEntry.weight; + brewFlow.old_weight = 0; + brewFlow.actual_smoothed_weight = 0; + brewFlow.old_smoothed_weight = 0; + brewFlow.not_mutated_weight = 0; + newBrewFlow.weight.push(brewFlow); + + if (shotEntry.weight > 0 && firstDripTimeSet === false) { + firstDripTimeSet = true; + + this.brewComponent.brewFirstDripTime?.setTime(seconds, milliseconds); + this.brewComponent.brewFirstDripTime?.changeEvent(); + } + + const pressureFlow: IBrewPressureFlow = {} as IBrewPressureFlow; + pressureFlow.timestamp = timestamp; + pressureFlow.brew_time = ''; + pressureFlow.actual_pressure = shotEntry.pressure; + pressureFlow.old_pressure = 0; + newBrewFlow.pressureFlow.push(pressureFlow); + + const temperatureFlow: IBrewTemperatureFlow = {} as IBrewTemperatureFlow; + temperatureFlow.timestamp = timestamp; + temperatureFlow.brew_time = ''; + temperatureFlow.actual_temperature = shotEntry.temperature; + temperatureFlow.old_temperature = 0; + newBrewFlow.temperatureFlow.push(temperatureFlow); + } + + const lastEntry = newBrewFlow.weight[newBrewFlow.weight.length - 1]; + this.brewComponent.data.brew_beverage_quantity = lastEntry.actual_weight; + + this.brewComponent.timer?.setTime(seconds, milliseconds); + this.brewComponent.timer?.changeEvent(); + + this.brewComponent.brewBrewingGraphEl.flow_profile_raw = newBrewFlow; + this.brewComponent.brewBrewingGraphEl.initializeFlowChart(true); + } + public getPreparationDeviceType() { if (this.preparationDevice instanceof XeniaDevice) { return PreparationDeviceType.XENIA; } else if (this.preparationDevice instanceof MeticulousDevice) { return PreparationDeviceType.METICULOUS; + } else if (this.preparationDevice instanceof SanremoYOUDevice) { + return PreparationDeviceType.SANREMO_YOU; } return PreparationDeviceType.NONE; } @@ -309,4 +434,6 @@ export class BrewBrewingPreparationDeviceComponent implements OnInit { } }, 50); } + + protected readonly SanremoYOUMode = SanremoYOUMode; } diff --git a/src/components/graph-display-card/graph-display-card.component.ts b/src/components/graph-display-card/graph-display-card.component.ts index 64c389208..023557a70 100644 --- a/src/components/graph-display-card/graph-display-card.component.ts +++ b/src/components/graph-display-card/graph-display-card.component.ts @@ -15,6 +15,8 @@ import { UIHelper } from '../../services/uiHelper'; import { UIFileHelper } from '../../services/uiFileHelper'; import { Platform } from '@ionic/angular'; import { UISettingsStorage } from '../../services/uiSettingsStorage'; +import { HistoryListingEntry } from '@meticulous-home/espresso-api/dist/types'; +import { MeticulousDevice } from '../../classes/preparationDevice/meticulous/meticulousDevice'; declare var Plotly; @Component({ @@ -26,7 +28,10 @@ export class GraphDisplayCardComponent implements OnInit { @Input() public flowProfileData: any; @Input() public flowProfilePath: any; + @Input() public meticulousHistoryData: HistoryListingEntry; + @Input() public chartWidth: number; + @Input() public chartHeight: number; public flow_profile_raw: BrewFlow = new BrewFlow(); @@ -56,8 +61,12 @@ export class GraphDisplayCardComponent implements OnInit { this.settings = this.uiSettingsStorage.getSettings(); if (this.flowProfilePath) { await this.readFlowProfile(); - } else { + } else if (this.flowProfileData) { this.flow_profile_raw = this.uiHelper.cloneData(this.flowProfileData); + } else if (this.meticulousHistoryData) { + this.flow_profile_raw = MeticulousDevice.returnBrewFlowForShotData( + this.meticulousHistoryData.data + ); } setTimeout(() => { this.initializeFlowChart(); @@ -89,7 +98,10 @@ export class GraphDisplayCardComponent implements OnInit { chartWidth = this.chartWidth; } - const chartHeight: number = 150; + let chartHeight: number = 150; + if (this.chartHeight) { + chartHeight = this.chartHeight; + } let tickFormat = '%S'; @@ -151,6 +163,16 @@ export class GraphDisplayCardComponent implements OnInit { }, }; + const graph_pressure_settings = this.settings.graph_pressure; + const suggestedMinPressure: number = graph_pressure_settings.lower; + let suggestedMaxPressure = graph_pressure_settings.upper; + try { + if (this.pressureTrace?.y.length > 0) { + suggestedMaxPressure = Math.max(...this.pressureTrace.y); + suggestedMaxPressure = Math.ceil(suggestedMaxPressure + 1); + } + } catch (ex) {} + layout['yaxis4'] = { title: '', titlefont: { color: '#05C793' }, @@ -160,8 +182,8 @@ export class GraphDisplayCardComponent implements OnInit { side: 'right', fixedrange: true, showgrid: false, + range: [suggestedMinPressure, suggestedMaxPressure], position: 0.93, - range: [0, 10], visible: true, }; @@ -205,7 +227,6 @@ export class GraphDisplayCardComponent implements OnInit { try { Plotly.purge(this.profileDiv.nativeElement); } catch (ex) {} - const graphSettings = this.settings.graph.FILTER; this.weightTrace = { x: [], diff --git a/src/components/photo-add/photo-add.component.html b/src/components/photo-add/photo-add.component.html index c2975a83e..099304c2e 100644 --- a/src/components/photo-add/photo-add.component.html +++ b/src/components/photo-add/photo-add.component.html @@ -2,7 +2,7 @@  {{"ADD_PHOTO" | translate}}
- + diff --git a/src/components/photo-add/photo-add.component.ts b/src/components/photo-add/photo-add.component.ts index 273ebc951..ff93088a8 100644 --- a/src/components/photo-add/photo-add.component.ts +++ b/src/components/photo-add/photo-add.component.ts @@ -3,6 +3,7 @@ import { ElementRef, EventEmitter, Input, + OnDestroy, OnInit, Output, ViewChild, @@ -24,7 +25,7 @@ import { TranslateService } from '@ngx-translate/core'; templateUrl: './photo-add.component.html', styleUrls: ['./photo-add.component.scss'], }) -export class PhotoAddComponent implements OnInit { +export class PhotoAddComponent implements OnInit, OnDestroy { @Input() public data: Brew | Bean | GreenBean | Mill | Preparation; @Output() public dataChange = new EventEmitter< Brew | Bean | GreenBean | Mill | Preparation @@ -47,7 +48,7 @@ export class PhotoAddComponent implements OnInit { this.updateSlider(); }, 250); } - + public ngOnDestroy() {} public addImage(): void { this.uiImage.showOptionChooser().then((_option) => { if (_option === 'CHOOSE') { diff --git a/src/components/photo-view/photo-view.component.html b/src/components/photo-view/photo-view.component.html index eec878722..9b25041d7 100644 --- a/src/components/photo-view/photo-view.component.html +++ b/src/components/photo-view/photo-view.component.html @@ -1,5 +1,5 @@
- + diff --git a/src/components/preparation-information-card/preparation-information-card.component.ts b/src/components/preparation-information-card/preparation-information-card.component.ts index 578bbdcd2..b07e55ba3 100644 --- a/src/components/preparation-information-card/preparation-information-card.component.ts +++ b/src/components/preparation-information-card/preparation-information-card.component.ts @@ -217,17 +217,7 @@ export class PreparationInformationCardComponent implements OnInit { } public async connectDevice() { - this.uiAnalytics.trackEvent( - PREPARATION_TRACKING.TITLE, - PREPARATION_TRACKING.ACTIONS.CONNECT_DEVICE - ); - const modal = await this.modalController.create({ - component: PreparationConnectedDeviceComponent, - componentProps: { preparation: this.preparation }, - id: PreparationConnectedDeviceComponent.COMPONENT_ID, - }); - await modal.present(); - await modal.onWillDismiss(); + await this.uiPreparationHelper.connectDevice(this.preparation); } public async longPressEditPreparation(event) { diff --git a/src/enums/beans/beanSortAfter.ts b/src/enums/beans/beanSortAfter.ts index 9c324f15f..5b0427365 100755 --- a/src/enums/beans/beanSortAfter.ts +++ b/src/enums/beans/beanSortAfter.ts @@ -5,6 +5,6 @@ export enum BEAN_SORT_AFTER { ROASTING_DATE = 'ROASTING DATE', CREATION_DATE = 'CREATION DATE', PURCHASE_DATE = 'PURCHASE DATE', - RATING ='RATING' - + RATING = 'RATING', + BEAN_AGE = 'BEAN AGE', } diff --git a/src/enums/brews/brewAction.ts b/src/enums/brews/brewAction.ts index 96dc6f74a..3cc746fa9 100755 --- a/src/enums/brews/brewAction.ts +++ b/src/enums/brews/brewAction.ts @@ -14,4 +14,5 @@ export enum BREW_ACTION { RATING = 'RATING', TOGGLE_BEST_BREW = 'TOGGLE_BEST_BREW', SHOW_VISUALIZER = 'SHOW_VISUALIZER', + SHOW_GRAPH = 'SHOW_GRAPH', } diff --git a/src/enums/preparationDevice/sanremo/sanremoYOUMode.ts b/src/enums/preparationDevice/sanremo/sanremoYOUMode.ts new file mode 100755 index 000000000..c66e9caa4 --- /dev/null +++ b/src/enums/preparationDevice/sanremo/sanremoYOUMode.ts @@ -0,0 +1,4 @@ +export enum SanremoYOUMode { + LISTENING = 'LISTENING', + CONTROLLING = 'CONTROLLING', +} diff --git a/src/enums/preparations/preparationTypes.ts b/src/enums/preparations/preparationTypes.ts index 57f5e0413..140e7d282 100755 --- a/src/enums/preparations/preparationTypes.ts +++ b/src/enums/preparations/preparationTypes.ts @@ -1,5 +1,8 @@ export enum PREPARATION_TYPES { CUSTOM_PREPARATION = 'CUSTOM_PREPARATION', + METICULOUS = 'METICULOUS', + SANREMO_YOU = 'SANREMO_YOU', + XENIA = 'XENIA', AEROPRESS = 'AEROPRESS', V60 = 'V60', CHEMEX = 'CHEMEX', @@ -40,5 +43,4 @@ export enum PREPARATION_TYPES { ROK = 'ROK', TORNADO_DUO = 'TORNADO_DUO', TRICOLATE = 'TRICOLATE', - METICULOUS = 'METICULOUS', } diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 2a39fae03..073bb2299 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,4 +1,7 @@ export const environment = { production: true, - API_URL: 'https://backend.beanconqueror.com/' + API_URL: 'https://backend.beanconqueror.com/', + FEATURES_ACTIVE: { + SANREMO_YOU: false, + }, }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index faa76bbe0..0c4481274 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -5,6 +5,9 @@ export const environment = { production: false, API_URL: 'https://backend.beanconqueror.com/', + FEATURES_ACTIVE: { + SANREMO_YOU: false, + }, }; /* diff --git a/src/interfaces/preparationDevices/sanremoYOU/iSanremoYOUParams.ts b/src/interfaces/preparationDevices/sanremoYOU/iSanremoYOUParams.ts new file mode 100644 index 000000000..f753a6389 --- /dev/null +++ b/src/interfaces/preparationDevices/sanremoYOU/iSanremoYOUParams.ts @@ -0,0 +1,7 @@ +import { SanremoYOUMode } from '../../../enums/preparationDevice/sanremo/sanremoYOUMode'; + +export interface ISanremoYOUParams { + stopAtWeight: number; + residualLagTime: number; + selectedMode: SanremoYOUMode; +} diff --git a/src/interfaces/settings/iSettings.ts b/src/interfaces/settings/iSettings.ts index 60396ee0d..35d8d7c12 100755 --- a/src/interfaces/settings/iSettings.ts +++ b/src/interfaces/settings/iSettings.ts @@ -84,6 +84,16 @@ export interface ISettings { ARCHIVED: IBeanPageSort; }; + bean_collapsed: { + OPEN: boolean; + ARCHIVED: boolean; + FROZEN: boolean; + }; + brew_collapsed: { + OPEN: boolean; + ARCHIVED: boolean; + }; + green_bean_sort: { OPEN: IBeanPageSort; ARCHIVED: IBeanPageSort; @@ -125,6 +135,10 @@ export interface ISettings { lower: number; }; }; + graph_pressure: { + upper: number; + lower: number; + }; welcome_page_showed: boolean; diff --git a/src/popover/data-corruption-found/data-corruption-found.component.html b/src/popover/data-corruption-found/data-corruption-found.component.html new file mode 100644 index 000000000..8f17dd60e --- /dev/null +++ b/src/popover/data-corruption-found/data-corruption-found.component.html @@ -0,0 +1,69 @@ + + Data corruption found + + + + + + It somehow looks that your current data are corrupted, we've checked the backup file, in the following list you'll see how much data you've stored actually and how many are stored in your backup.
+ You can then choose to restore this backup or not.
+ If you don't feel save right now please make a screenshot of this screen, contact info@beanconqueror.com and close this app completely - any issues will be prevent with this +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Key + Actual Storage + + Backup +
Beans{{actualUIStorageDataObj?.BEANS}}{{backupDataObj?.BEANS?.length}}
Brews{{actualUIStorageDataObj?.BREWS}}{{backupDataObj?.BREWS?.length}}
Preparation{{actualUIStorageDataObj?.PREPARATION}}{{backupDataObj?.PREPARATION?.length}}
Grinder{{actualUIStorageDataObj?.MILL}}{{backupDataObj?.MILL?.length}}
+
+
+
+ + + + + Dont import backup + + + Import backup + + + + + diff --git a/src/popover/data-corruption-found/data-corruption-found.component.scss b/src/popover/data-corruption-found/data-corruption-found.component.scss new file mode 100644 index 000000000..25eeb2a52 --- /dev/null +++ b/src/popover/data-corruption-found/data-corruption-found.component.scss @@ -0,0 +1,6 @@ +:host { + table { + width:100%; + text-align:left; + } +} diff --git a/src/popover/data-corruption-found/data-corruption-found.component.spec.ts b/src/popover/data-corruption-found/data-corruption-found.component.spec.ts new file mode 100644 index 000000000..6962cefa5 --- /dev/null +++ b/src/popover/data-corruption-found/data-corruption-found.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { DataCorruptionFoundComponent } from './data-corruption-found.component'; + +describe('DataCorruptionFoundComponent', () => { + let component: DataCorruptionFoundComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [DataCorruptionFoundComponent], + imports: [IonicModule.forRoot()], + }).compileComponents(); + + fixture = TestBed.createComponent(DataCorruptionFoundComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/popover/data-corruption-found/data-corruption-found.component.ts b/src/popover/data-corruption-found/data-corruption-found.component.ts new file mode 100644 index 000000000..30fc8e56e --- /dev/null +++ b/src/popover/data-corruption-found/data-corruption-found.component.ts @@ -0,0 +1,61 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { ModalController, Platform } from '@ionic/angular'; + +@Component({ + selector: 'app-data-corruption-found', + templateUrl: './data-corruption-found.component.html', + styleUrls: ['./data-corruption-found.component.scss'], +}) +export class DataCorruptionFoundComponent implements OnInit { + public static POPOVER_ID: string = 'data-corruption-found-popover'; + + @Input() public actualUIStorageDataObj: any = undefined; + @Input() public backupDataObj: any = undefined; + + private disableHardwareBack; + + constructor( + private readonly modalController: ModalController, + private readonly platform: Platform + ) {} + + public ngOnInit() { + try { + this.disableHardwareBack = this.platform.backButton.subscribeWithPriority( + 9999, + (processNextHandler) => { + // Don't do anything. + } + ); + } catch (ex) {} + } + + public async dismiss() { + try { + this.disableHardwareBack.unsubscribe(); + } catch (ex) {} + + this.modalController.dismiss( + { + dismissed: true, + }, + undefined, + DataCorruptionFoundComponent.POPOVER_ID + ); + } + + public async import() { + try { + this.disableHardwareBack.unsubscribe(); + } catch (ex) {} + + this.modalController.dismiss( + { + dismissed: true, + import: true, + }, + undefined, + DataCorruptionFoundComponent.POPOVER_ID + ); + } +} diff --git a/src/popover/photo-popover/photo-popover.component.html b/src/popover/photo-popover/photo-popover.component.html index ecb0bc64c..feb076c2d 100644 --- a/src/popover/photo-popover/photo-popover.component.html +++ b/src/popover/photo-popover/photo-popover.component.html @@ -10,7 +10,7 @@
- + diff --git a/src/popover/update-popover/update-popover.component.html b/src/popover/update-popover/update-popover.component.html index 5cb174de2..376a1bb6f 100644 --- a/src/popover/update-popover/update-popover.component.html +++ b/src/popover/update-popover/update-popover.component.html @@ -1,6 +1,6 @@ - +
diff --git a/src/popover/welcome-popover/welcome-popover.component.html b/src/popover/welcome-popover/welcome-popover.component.html index 97da5ca43..4098a24a4 100644 --- a/src/popover/welcome-popover/welcome-popover.component.html +++ b/src/popover/welcome-popover/welcome-popover.component.html @@ -30,7 +30,7 @@ - +
diff --git a/src/popover/welcome-popover/welcome-popover.component.ts b/src/popover/welcome-popover/welcome-popover.component.ts index 38c67d8ad..25145648a 100644 --- a/src/popover/welcome-popover/welcome-popover.component.ts +++ b/src/popover/welcome-popover/welcome-popover.component.ts @@ -12,12 +12,6 @@ import { UIPreparationHelper } from '../../services/uiPreparationHelper'; styleUrls: ['./welcome-popover.component.scss'], }) export class WelcomePopoverComponent implements OnInit { - public slideOpts = { - allowTouchMove: false, - speed: 400, - slide: 4, - }; - public slide: number = 1; @ViewChild('slider', { static: false }) public welcomeSlider: | ElementRef diff --git a/src/services/coffeeBluetoothDevices/coffee-bluetooth-devices.service.ts b/src/services/coffeeBluetoothDevices/coffee-bluetooth-devices.service.ts index 39f104380..a23dd3d47 100644 --- a/src/services/coffeeBluetoothDevices/coffee-bluetooth-devices.service.ts +++ b/src/services/coffeeBluetoothDevices/coffee-bluetooth-devices.service.ts @@ -41,6 +41,7 @@ import { BookooPressure } from 'src/classes/devices/bookooPressure'; import { BasicGrillThermometer } from 'src/classes/devices/basicGrillThermometer'; import { MeaterThermometer } from 'src/classes/devices/meaterThermometer'; import { CombustionThermometer } from '../../classes/devices/combustionThermometer'; +import { ArgosThermometer } from '../../classes/devices/argosThermometer'; declare var device: any; declare var ble: any; @@ -546,7 +547,8 @@ export class CoffeeBluetoothDevicesService { ETITemperature.test(scanDevice) || BasicGrillThermometer.test(scanDevice) || MeaterThermometer.test(scanDevice) || - CombustionThermometer.test(scanDevice) + CombustionThermometer.test(scanDevice) || + ArgosThermometer.test(scanDevice) ) { // We found all needed devices. promiseResolved = true; @@ -1046,6 +1048,15 @@ export class CoffeeBluetoothDevicesService { id: deviceTemperature.id, type: TemperatureType.COMBUSTION, }); + } else if (ArgosThermometer.test(deviceTemperature)) { + this.logger.log( + 'BleManager - We found a Argos Thermometer device ' + + JSON.stringify(deviceTemperature) + ); + supportedDevices.push({ + id: deviceTemperature.id, + type: TemperatureType.ARGOS, + }); } } resolve(supportedDevices); @@ -1091,6 +1102,15 @@ export class CoffeeBluetoothDevicesService { type: TemperatureType.COMBUSTION, }); return; + } else if (ArgosThermometer.test(deviceTemperature)) { + this.logger.log( + 'BleManager - We found a Argos Thermometer device ' + ); + resolve({ + id: deviceTemperature.id, + type: TemperatureType.ARGOS, + }); + return; } } resolve(undefined); diff --git a/src/services/uiBeanHelper.ts b/src/services/uiBeanHelper.ts index 6c3980d54..be5600be8 100644 --- a/src/services/uiBeanHelper.ts +++ b/src/services/uiBeanHelper.ts @@ -29,6 +29,8 @@ import { BEAN_ROASTING_TYPE_ENUM } from '../enums/beans/beanRoastingType'; import { AssociatedBrewsComponent } from '../app/brew/associated-brews/associated-brews.component'; import { BrewCuppingComponent } from '../app/brew/brew-cupping/brew-cupping.component'; import { BeanPopoverFreezeComponent } from '../app/beans/bean-popover-freeze/bean-popover-freeze.component'; +import { BeanPopoverFrozenListComponent } from '../app/beans/bean-popover-frozen-list/bean-popover-frozen-list.component'; +import { BeanPopoverListComponent } from '../app/beans/bean-popover-list/bean-popover-list.component'; /** * Handles every helping functionalities @@ -319,6 +321,16 @@ export class UIBeanHelper { await modal.onWillDismiss(); } + public async showBeans(_beanList: Array) { + const modal = await this.modalController.create({ + component: BeanPopoverListComponent, + id: BeanPopoverListComponent.COMPONENT_ID, + componentProps: { beansList: _beanList }, + }); + await modal.present(); + await modal.onWillDismiss(); + } + public async editBean(_bean: Bean) { const modal = await this.modalController.create({ component: BeansEditComponent, diff --git a/src/services/uiExcel.ts b/src/services/uiExcel.ts index d52ebf160..d8f3f7f32 100755 --- a/src/services/uiExcel.ts +++ b/src/services/uiExcel.ts @@ -15,7 +15,6 @@ import { BEAN_ROASTING_TYPE_ENUM } from '../enums/beans/beanRoastingType'; import { BEAN_MIX_ENUM } from '../enums/beans/mix'; import { UIPreparationStorage } from './uiPreparationStorage'; import { UIAlert } from './uiAlert'; -import { SocialSharing } from '@awesome-cordova-plugins/social-sharing/ngx'; import { UIFileHelper } from './uiFileHelper'; import { UIMillStorage } from './uiMillStorage'; import { BrewFlow, IBrewWaterFlow } from '../classes/brew/brewFlow'; @@ -23,6 +22,11 @@ import moment from 'moment'; import { UISettingsStorage } from './uiSettingsStorage'; import { Settings } from '../classes/settings/settings'; import { Brew } from '../classes/brew/brew'; +import { Bean } from '../classes/bean/bean'; +import { IBeanInformation } from '../interfaces/bean/iBeanInformation'; +import { UIBeanHelper } from './uiBeanHelper'; +import { GreenBean } from '../classes/green-bean/green-bean'; +import { UIGreenBeanStorage } from './uiGreenBeanStorage'; @Injectable({ providedIn: 'root', @@ -37,14 +41,14 @@ export class UIExcel { private readonly platform: Platform, private readonly uiBrewStorage: UIBrewStorage, private readonly uiBeanStorage: UIBeanStorage, + private readonly uiGreenBeanStorage: UIGreenBeanStorage, private readonly uiPreparationStoraage: UIPreparationStorage, private readonly translate: TranslateService, private readonly uiAlert: UIAlert, - private readonly socialsharing: SocialSharing, private readonly uiFileHelper: UIFileHelper, private readonly uiMillStorage: UIMillStorage, - private readonly alertCtrl: AlertController, - private readonly uiSettingsStorage: UISettingsStorage + private readonly uiSettingsStorage: UISettingsStorage, + private readonly uiBeanHelper: UIBeanHelper ) { this.settings = this.uiSettingsStorage.getSettings(); } @@ -80,8 +84,6 @@ export class UIExcel { const wsData: any[][] = [header]; for (const entry of _flow.weight) { - const notMutatedWeight: number = 0; - const wbEntry: Array = [ entry.timestamp, entry.brew_time, @@ -719,18 +721,530 @@ export class UIExcel { } } + public async importGreenBeansByExcel(_arrayBuffer) { + try { + /* data is an ArrayBuffer */ + const wb = XLSX.read(_arrayBuffer); + const data = XLSX.utils.sheet_to_json(wb.Sheets['Green Beans']); + + let varietyInformationWhereAdded: boolean = false; + const addedBeans: Array = []; + const toAddBeans: Array = []; + for (const entry of data) { + /** + * 1. Bean certification: 14 + * 1. Country: 5 + * 1. Elevation: 9 + * 1. Farm: 7 + * 1. Farmer: 8 + * 1. Harvested: 12 + * 1. Percentage: 13 + * 1. Processing: 11 + * 1. Region: 6 + * 1. Variety: 10 + * 1. Fob Price + * 1. Purchasing Price + * Archived: false - + * Cost: 13 - + * Cupping points: 92 - + * Decaffeinated: false - + * EAN / Articlenumber: 2 - + * Flavour profile: "Red, whine, apple" - + * Name: 123 - + * Notes: 3 + * Rating: 4 + * Website: 1 - + * Weight: 250 - + * Buy Date + */ + + const bean: GreenBean = new GreenBean(); + + const nameEntry = entry['Name']; + if (nameEntry) { + bean.name = nameEntry.toString(); + } else { + continue; + } + + const weightEntry = entry['Weight']; + if (weightEntry && Number(weightEntry) > 0) { + bean.weight = Number(weightEntry); + } + + const buyDateEntry = entry['Buy Date']; + if (buyDateEntry && Number(buyDateEntry) > 0) { + bean.date = this.getJsDateFromExcel(Number(buyDateEntry)); + } + + const websiteEntry = entry['Website']; + if (websiteEntry) { + bean.url = websiteEntry.toString(); + } + + const flavourProfileEntry = entry['Flavour profile']; + if (flavourProfileEntry) { + bean.aromatics = flavourProfileEntry.toString(); + } + + const eanArticleNumberEntry = entry['EAN / Articlenumber']; + if (eanArticleNumberEntry) { + bean.ean_article_number = eanArticleNumberEntry.toString(); + } + + const decaffeinatedEntry = entry['Decaffeinated']; + if (decaffeinatedEntry) { + bean.decaffeinated = decaffeinatedEntry; + } + + const cuppingPointsEntry = entry['Cupping points']; + if (cuppingPointsEntry) { + bean.cupping_points = cuppingPointsEntry.toString(); + } + + const costEntry = entry['Cost']; + if (costEntry) { + bean.cost = Number(costEntry); + } + + const archivedEntry = entry['Archived']; + if (archivedEntry) { + bean.finished = archivedEntry; + } + + const beanInformation: IBeanInformation = {} as IBeanInformation; + let hasOneBeanInformation: boolean = false; + + const informationCertificationEntry = entry['1. Bean certification']; + if (informationCertificationEntry) { + beanInformation.certification = + informationCertificationEntry.toString(); + hasOneBeanInformation = true; + } + + const informationCountryEntry = entry['1. Country']; + if (informationCountryEntry) { + beanInformation.country = informationCountryEntry.toString(); + hasOneBeanInformation = true; + } + + const informationElevationEntry = entry['1. Elevation']; + if (informationElevationEntry) { + beanInformation.elevation = informationElevationEntry.toString(); + hasOneBeanInformation = true; + } + + const informationFarmEntry = entry['1. Farm']; + if (informationFarmEntry) { + beanInformation.farm = informationFarmEntry.toString(); + hasOneBeanInformation = true; + } + + const informationFarmerEntry = entry['1. Farmer']; + if (informationFarmerEntry) { + beanInformation.farmer = informationFarmerEntry.toString(); + hasOneBeanInformation = true; + } + + const informationHarvestedEntry = entry['1. Harvested']; + if (informationHarvestedEntry) { + beanInformation.harvest_time = informationHarvestedEntry.toString(); + hasOneBeanInformation = true; + } + + const informationPercentageEntry = entry['1. Percentage']; + if ( + informationPercentageEntry && + Number(informationPercentageEntry) > 0 + ) { + beanInformation.percentage = Number(informationPercentageEntry); + hasOneBeanInformation = true; + } + + const informationProcessingEntry = entry['1. Processing']; + if (informationProcessingEntry) { + beanInformation.processing = informationProcessingEntry.toString(); + hasOneBeanInformation = true; + } + + const informationRegionEntry = entry['1. Region']; + if (informationRegionEntry) { + beanInformation.region = informationRegionEntry.toString(); + hasOneBeanInformation = true; + } + + const informationVarietyEntry = entry['1. Variety']; + if (informationVarietyEntry) { + beanInformation.variety = informationVarietyEntry.toString(); + hasOneBeanInformation = true; + } + + const informationFobPriceEntry = entry['1. Fob Price']; + if (informationFobPriceEntry && Number(informationFobPriceEntry) > 0) { + beanInformation.fob_price = Number(informationFobPriceEntry); + hasOneBeanInformation = true; + } + + const informationPurchasingPriceEntry = entry['1. Purchasing Price']; + if ( + informationPurchasingPriceEntry && + Number(informationPurchasingPriceEntry) > 0 + ) { + beanInformation.purchasing_price = Number( + informationPurchasingPriceEntry + ); + hasOneBeanInformation = true; + } + + if (hasOneBeanInformation) { + varietyInformationWhereAdded = true; + bean.bean_information.push(beanInformation); + } else { + /** Add atleast one empty bean information**/ + const emptyBeanInformation: IBeanInformation = {} as IBeanInformation; + bean.bean_information.push(emptyBeanInformation); + } + + toAddBeans.push(bean); + } + + if (varietyInformationWhereAdded) { + if (this.settings.bean_manage_parameters.bean_information === false) { + this.settings.bean_manage_parameters.bean_information = true; + await this.uiSettingsStorage.saveSettings(this.settings); + } + } + + /** + * Add all beans afterwards to not have some added and then going into an exception + */ + for await (const addBean of toAddBeans) { + try { + const newBean: GreenBean = await this.uiGreenBeanStorage.add(addBean); + addedBeans.push(newBean); + } catch (ex) {} + } + + if (addedBeans.length > 0) { + try { + await this.uiAlert.showMessage( + 'GREEN_BEANS_IMPORTED_SUCCESSFULLY_DESCRIPTION', + 'IMPORT_SUCCESSFULLY', + undefined, + true + ); + } catch (ex) {} + } else { + this.uiAlert.showMessage( + 'BEANS_IMPORTED_UNSUCCESSFULLY_WRONG_EXCELFILE', + 'IMPORT_UNSUCCESSFULLY', + 'OK', + false + ); + } + } catch (ex) { + this.uiAlert.showMessage( + ex.message, + this.translate.instant('IMPORT_UNSUCCESSFULLY'), + this.translate.instant('OK'), + false + ); + } + } + public async importBeansByExcel(_arrayBuffer) { try { /* data is an ArrayBuffer */ const wb = XLSX.read(_arrayBuffer); const data = XLSX.utils.sheet_to_json(wb.Sheets['Beans']); + + let isOneEntryFrozen: boolean = false; + let varietyInformationWhereAdded: boolean = false; + const addedBeans: Array = []; + const toAddBeans: Array = []; for (const entry of data) { - //const bean: Bean = new Bean(); - //bean.bean_information + /** + * 1. Bean certification: 14 + * 1. Country: 5 + * 1. Elevation: 9 + * 1. Farm: 7 + * 1. Farmer: 8 + * 1. Harvested: 12 + * 1. Percentage: 13 + * 1. Processing: 11 + * 1. Region: 6 + * 1. Variety: 10 + * 1. Fob Price + * 1. Purchasing Price + * Archived: false - + * Blend: "UNKNOWN" - + * Cost: 13 - + * Cupping points: 92 - + * Decaffeinated: false - + * Degree of Roast: "UNKNOWN" - + * EAN / Articlenumber: 2 - + * Flavour profile: "Red, whine, apple" - + * Name: 123 - + * Notes: 3 + * Rating: 4 + * Roast date: 44593 + * Roast type: "FILTER" - + * Roaster: 123 - + * Website: 1 - + * Weight: 250 - + * Frozen Date + * Unfrozen Date + * Freezing Storage Type + * Frozen Note + */ + + const bean: Bean = new Bean(); + + const nameEntry = entry['Name']; + if (nameEntry) { + bean.name = nameEntry.toString(); + } else { + continue; + } + + const weightEntry = entry['Weight']; + if (weightEntry && Number(weightEntry) > 0) { + bean.weight = Number(weightEntry); + } + + const roastDateEntry = entry['Roast date']; + if (roastDateEntry && Number(roastDateEntry) > 0) { + bean.roastingDate = this.getJsDateFromExcel(Number(roastDateEntry)); + } + + const websiteEntry = entry['Website']; + if (websiteEntry) { + bean.url = websiteEntry.toString(); + } + + const roasterEntry = entry['Roaster']; + if (roasterEntry) { + bean.roaster = roasterEntry.toString(); + } + + const roastTypeEntry = entry['Roast type']; + if (roastTypeEntry) { + bean.bean_roasting_type = roastTypeEntry; + } + + const flavourProfileEntry = entry['Flavour profile']; + if (flavourProfileEntry) { + bean.aromatics = flavourProfileEntry.toString(); + } + + const eanArticleNumberEntry = entry['EAN / Articlenumber']; + if (eanArticleNumberEntry) { + bean.ean_article_number = eanArticleNumberEntry.toString(); + } + + const degreeOfRoastEntry = entry['Degree of Roast']; + if (degreeOfRoastEntry) { + bean.roast = degreeOfRoastEntry; + } + const decaffeinatedEntry = entry['Decaffeinated']; + if (decaffeinatedEntry) { + bean.decaffeinated = decaffeinatedEntry; + } + + const cuppingPointsEntry = entry['Cupping points']; + if (cuppingPointsEntry) { + bean.cupping_points = cuppingPointsEntry.toString(); + } + + const costEntry = entry['Cost']; + if (costEntry) { + bean.cost = Number(costEntry); + } + + const blendEntry = entry['Blend']; + if (blendEntry) { + bean.beanMix = blendEntry; + } + + const archivedEntry = entry['Archived']; + if (archivedEntry) { + bean.finished = archivedEntry; + } + + const beanInformation: IBeanInformation = {} as IBeanInformation; + let hasOneBeanInformation: boolean = false; + + const informationCertificationEntry = entry['1. Bean certification']; + if (informationCertificationEntry) { + beanInformation.certification = + informationCertificationEntry.toString(); + hasOneBeanInformation = true; + } + + const informationCountryEntry = entry['1. Country']; + if (informationCountryEntry) { + beanInformation.country = informationCountryEntry.toString(); + hasOneBeanInformation = true; + } + + const informationElevationEntry = entry['1. Elevation']; + if (informationElevationEntry) { + beanInformation.elevation = informationElevationEntry.toString(); + hasOneBeanInformation = true; + } + + const informationFarmEntry = entry['1. Farm']; + if (informationFarmEntry) { + beanInformation.farm = informationFarmEntry.toString(); + hasOneBeanInformation = true; + } + + const informationFarmerEntry = entry['1. Farmer']; + if (informationFarmerEntry) { + beanInformation.farmer = informationFarmerEntry.toString(); + hasOneBeanInformation = true; + } + + const informationHarvestedEntry = entry['1. Harvested']; + if (informationHarvestedEntry) { + beanInformation.harvest_time = informationHarvestedEntry.toString(); + hasOneBeanInformation = true; + } + + const informationPercentageEntry = entry['1. Percentage']; + if ( + informationPercentageEntry && + Number(informationPercentageEntry) > 0 + ) { + beanInformation.percentage = Number(informationPercentageEntry); + hasOneBeanInformation = true; + } + + const informationProcessingEntry = entry['1. Processing']; + if (informationProcessingEntry) { + beanInformation.processing = informationProcessingEntry.toString(); + hasOneBeanInformation = true; + } + + const informationRegionEntry = entry['1. Region']; + if (informationRegionEntry) { + beanInformation.region = informationRegionEntry.toString(); + hasOneBeanInformation = true; + } + + const informationVarietyEntry = entry['1. Variety']; + if (informationVarietyEntry) { + beanInformation.variety = informationVarietyEntry.toString(); + hasOneBeanInformation = true; + } + + const informationFobPriceEntry = entry['1. Fob Price']; + if (informationFobPriceEntry && Number(informationFobPriceEntry) > 0) { + beanInformation.fob_price = Number(informationFobPriceEntry); + hasOneBeanInformation = true; + } + + const informationPurchasingPriceEntry = entry['1. Purchasing Price']; + if ( + informationPurchasingPriceEntry && + Number(informationPurchasingPriceEntry) > 0 + ) { + beanInformation.purchasing_price = Number( + informationPurchasingPriceEntry + ); + hasOneBeanInformation = true; + } + + if (hasOneBeanInformation) { + varietyInformationWhereAdded = true; + bean.bean_information.push(beanInformation); + } else { + /** Add atleast one empty bean information**/ + const emptyBeanInformation: IBeanInformation = {} as IBeanInformation; + bean.bean_information.push(emptyBeanInformation); + } + + const freezingStorageTypeEntry = entry['Freezing Storage Type']; + if (freezingStorageTypeEntry) { + bean.frozenStorageType = freezingStorageTypeEntry; + } + + const frozenNoteEntry = entry['Frozen Note']; + if (frozenNoteEntry) { + bean.frozenNote = frozenNoteEntry; + } + + const frozenDateEntry = entry['Frozen Date']; + if (frozenDateEntry && Number(frozenDateEntry) > 0) { + isOneEntryFrozen = true; + bean.frozenDate = this.getJsDateFromExcel(Number(frozenDateEntry)); + bean.frozenId = this.uiBeanHelper.generateFrozenId(); + } + + const unfrozenDateEntry = entry['Unfrozen Date']; + if (unfrozenDateEntry && Number(unfrozenDateEntry) > 0) { + isOneEntryFrozen = true; + bean.unfrozenDate = this.getJsDateFromExcel( + Number(unfrozenDateEntry) + ); + } + toAddBeans.push(bean); + } + + if (isOneEntryFrozen) { + // Activate the frozen feature if not active + if (this.settings.freeze_coffee_beans === false) { + this.settings.freeze_coffee_beans = true; + await this.uiSettingsStorage.saveSettings(this.settings); + } + } + if (varietyInformationWhereAdded) { + if (this.settings.bean_manage_parameters.bean_information === false) { + this.settings.bean_manage_parameters.bean_information = true; + await this.uiSettingsStorage.saveSettings(this.settings); + } + } + + /** + * Add all beans afterwards to not have some added and then going into an exception + */ + for await (const addBean of toAddBeans) { + try { + const newBean: Bean = await this.uiBeanStorage.add(addBean); + addedBeans.push(newBean); + } catch (ex) {} + } + + if (addedBeans.length > 0) { + try { + await this.uiAlert.showMessage( + 'BEANS_IMPORTED_SUCCESSFULLY_DESCRIPTION', + 'IMPORT_SUCCESSFULLY', + undefined, + true + ); + } catch (ex) {} + this.uiBeanHelper.showBeans(addedBeans); + } else { + this.uiAlert.showMessage( + 'BEANS_IMPORTED_UNSUCCESSFULLY_WRONG_EXCELFILE', + 'IMPORT_UNSUCCESSFULLY', + 'OK', + false + ); } - console.log(data); - console.log(wb); - } catch (ex) {} + } catch (ex) { + this.uiAlert.showMessage( + ex.message, + this.translate.instant('IMPORT_UNSUCCESSFULLY'), + this.translate.instant('OK'), + false + ); + } + } + + private getJsDateFromExcel(_dateNumber) { + return new Date((_dateNumber - (25567 + 2)) * 86400 * 1000).toISOString(); } /* Export button */ public async export() { diff --git a/src/services/uiExportImportHelper.ts b/src/services/uiExportImportHelper.ts index 07a6a274d..19c1d8468 100644 --- a/src/services/uiExportImportHelper.ts +++ b/src/services/uiExportImportHelper.ts @@ -20,11 +20,13 @@ import { import * as zip from '@zip.js/zip.js'; import { FileEntry } from '@awesome-cordova-plugins/file'; import { UILog } from './uiLog'; -import { Platform } from '@ionic/angular'; +import { ModalController, Platform } from '@ionic/angular'; import { UIFileHelper } from './uiFileHelper'; import { UIAlert } from './uiAlert'; import moment from 'moment'; import { UIBrewStorage } from './uiBrewStorage'; + +import { DataCorruptionFoundComponent } from '../popover/data-corruption-found/data-corruption-found.component'; @Injectable({ providedIn: 'root', }) @@ -43,7 +45,8 @@ export class UIExportImportHelper { private readonly uiFileHelper: UIFileHelper, private readonly uiAlert: UIAlert, private readonly uiSettingsStorage: UISettingsStorage, - private readonly uiBrewStorage: UIBrewStorage + private readonly uiBrewStorage: UIBrewStorage, + private readonly modalController: ModalController ) {} public async buildExportZIP(): Promise { @@ -239,6 +242,78 @@ export class UIExportImportHelper { }); }); } + private async checkBackupAndSeeIfDataAreCorrupted(_actualUIStorageDataObj) { + try { + this.uiLog.log( + 'checkBackupAndSeeIfDataAreCorrupted - Check if we got a deep corruption' + ); + const dataObj = _actualUIStorageDataObj.DATA; + const parsedJSON: any = await this.readBackupZIPFile(); + if (parsedJSON) { + let somethingCorrupted = false; + if (parsedJSON.BEANS?.length > dataObj.BEANS) { + somethingCorrupted = true; + } else if (parsedJSON.BREWS?.length > dataObj.BREWS) { + somethingCorrupted = true; + } else if (parsedJSON.PREPARATION?.length > dataObj.PREPARATION) { + somethingCorrupted = true; + } else if (parsedJSON.MILL?.length > dataObj.MILL) { + somethingCorrupted = true; + } + + this.uiLog.log( + 'checkBackupAndSeeIfDataAreCorrupted- Check over - if we got a deep corruption - Result: ' + + somethingCorrupted + ); + if (somethingCorrupted) { + const importBackup = await this.showDataCorruptionPopover( + dataObj, + parsedJSON + ); + if (importBackup) { + await this.importBackupJSON(parsedJSON); + } + } else { + this.uiLog.log( + "checkBackupAndSeeIfDataAreCorrupted - Check over - we didn't find any corrupted data" + ); + } + } else { + this.uiLog.log( + "checkBackupAndSeeIfDataAreCorrupted - We didn't found any json backup data so we can't do any checks" + ); + } + } catch (ex) { + this.uiLog.log( + 'Check over - if we got a deep corruption - Result exception: ' + + JSON.stringify(ex) + ); + } + } + + public async showDataCorruptionPopover( + _actualUIStorageDataObj, + _backupDataObj + ) { + const modal = await this.modalController.create({ + component: DataCorruptionFoundComponent, + id: DataCorruptionFoundComponent.POPOVER_ID, + componentProps: { + actualUIStorageDataObj: _actualUIStorageDataObj, + backupDataObj: _backupDataObj, + }, + }); + await modal.present(); + const returnData = await modal.onWillDismiss(); + this.uiLog.log( + 'Data corruption, choose to import: ' + returnData?.data?.import + ); + if (returnData?.data?.import) { + //User choose to import backup, go + return true; + } + return false; + } public async checkBackup() { try { @@ -248,90 +323,32 @@ export class UIExportImportHelper { this.uiLog.log('Check Backup'); const hasData = await this.uiStorage.hasData(); - let hasCorruptedData: boolean = false; + let actualUIStorageDataObj: any; if (hasData) { - hasCorruptedData = await this.uiStorage.hasCorruptedData(); + actualUIStorageDataObj = await this.uiStorage.hasCorruptedData(); } + this.uiLog.log('Check Backup - Has data ' + hasData); - if (!hasData || hasCorruptedData) { - this.uiLog.log( - 'Check Backup - No data are stored yet inside the app, so we try to find a backup file' - ); - // If we don't got any data, we check now if there is a Beanconqueror.zip saved. - this.uiFileHelper.getZIPFile('Beanconqueror.zip').then( - async (_arrayBuffer) => { - await this.uiAlert.showLoadingSpinner(); - try { - this.uiLog.log(' We found a backup, try to import'); - const parsedJSON = - await this.getJSONFromZIPArrayBufferContent(_arrayBuffer); - this.uiStorage.import(parsedJSON).then( - async () => { - this.uiLog.log('Sucessfully imported Backup'); - setTimeout(() => { - this.uiAlert.hideLoadingSpinner(); - }, 150); - resolve(null); - }, - () => { - this.uiLog.error('Could not import Backup'); - setTimeout(() => { - this.uiAlert.hideLoadingSpinner(); - }, 150); - resolve(null); - } - ); - } catch (ex) { - setTimeout(() => { - this.uiAlert.hideLoadingSpinner(); - }, 150); - } - }, - () => { - this.uiLog.log( - 'Check Backup - We couldnt retrieve any zip file - try the old JSON Way.' - ); - - this.uiFileHelper.getJSONFile('Beanconqueror.json').then( - async (_json) => { - await this.uiAlert.showLoadingSpinner(); - try { - this.uiLog.log('We found an backup, try to import'); - this.uiStorage.import(_json).then( - async () => { - this.uiLog.log('Sucessfully imported Backup'); - setTimeout(() => { - this.uiAlert.hideLoadingSpinner(); - }, 150); - resolve(null); - }, - () => { - this.uiLog.error('Could not import Backup'); - setTimeout(() => { - this.uiAlert.hideLoadingSpinner(); - }, 150); - resolve(null); - } - ); - } catch (ex) { - setTimeout(() => { - this.uiAlert.hideLoadingSpinner(); - }, 150); - } - }, - () => { - setTimeout(() => { - this.uiAlert.hideLoadingSpinner(); - }, 150); - this.uiLog.log( - 'Check Backup - We couldnt retrieve any JSON file' - ); - resolve(null); - } - ); - } - ); + if (!hasData || actualUIStorageDataObj.CORRUPTED) { + if (!hasData) { + this.uiLog.log( + 'Check Backup - We didnt found any data inside the app, so try to find a backup and import it' + ); + } else { + this.uiLog.log( + 'Check Backup - We found data but they where corrupted, so try to import a backup' + ); + } + + const parsedJSON = await this.readBackupZIPFile(); + if (parsedJSON) { + await this.importBackupJSON(parsedJSON); + } + resolve(null); } else { + await this.checkBackupAndSeeIfDataAreCorrupted( + actualUIStorageDataObj + ); resolve(null); } } else { @@ -345,6 +362,67 @@ export class UIExportImportHelper { } catch (ex) {} } + private importBackupJSON(_parsedJSON) { + const promise = new Promise(async (resolve, reject) => { + await this.uiAlert.showLoadingSpinner(); + + this.uiStorage.import(_parsedJSON).then( + async () => { + this.uiLog.log('Sucessfully imported Backup'); + setTimeout(() => { + this.uiAlert.hideLoadingSpinner(); + }, 150); + resolve(null); + }, + () => { + this.uiLog.error('Could not import Backup'); + setTimeout(() => { + this.uiAlert.hideLoadingSpinner(); + }, 150); + resolve(null); + } + ); + }); + return promise; + } + + private readBackupZIPFile() { + // If we don't got any data, we check now if there is a Beanconqueror.zip saved. + const promise = new Promise(async (resolve, reject) => { + this.uiFileHelper.getZIPFile('Beanconqueror.zip').then( + async (_arrayBuffer) => { + try { + this.uiLog.log('Read ZIP-File, we found an zip-file'); + const parsedJSON = await this.getJSONFromZIPArrayBufferContent( + _arrayBuffer + ); + resolve(parsedJSON); + } catch (ex) { + resolve(null); + } + }, + () => { + this.uiLog.log( + 'Read ZIP-FILE failed, try to read an old Beanconqueror.json' + ); + this.uiFileHelper.getJSONFile('Beanconqueror.json').then( + async (_json) => { + this.uiLog.log('Read ZIP-File, we found an json-file'); + resolve(_json); + }, + () => { + this.uiLog.log( + 'Check Backup - We couldnt retrieve any JSON file' + ); + resolve(null); + } + ); + } + ); + }); + return promise; + } + private getAutomatedBackupFilename(): string { return moment().format('DD_MM_YYYY').toString(); } diff --git a/src/services/uiFileHelper.ts b/src/services/uiFileHelper.ts index fbc075825..1cfe8c19d 100644 --- a/src/services/uiFileHelper.ts +++ b/src/services/uiFileHelper.ts @@ -847,10 +847,10 @@ export class UIFileHelper extends InstanceClass { ): Promise { return new Promise(async (resolve, reject) => { if (this.platform.is('cordova')) { - if (this.cachedInternalUrls[_filePath]) { + /** if (this.cachedInternalUrls[_filePath]) { //resolve(this.cachedInternalUrls[_filePath]); // return; - } + }**/ // let filePath: string; // filePath = _filePath; // filePath.slice(0, filePath.lastIndexOf('/')); @@ -884,8 +884,9 @@ export class UIFileHelper extends InstanceClass { this.domSanitizer.bypassSecurityTrustResourceUrl( convertedURL ); - this.cachedInternalUrls[_filePath] = convertedURL; - resolve(convertedURL); + const returningURL = convertedURL; + // this.cachedInternalUrls[_filePath] = convertedURL; + resolve(returningURL); }, () => { resolve(''); diff --git a/src/services/uiGraphHelper.ts b/src/services/uiGraphHelper.ts index 75fe25823..aa464a23f 100644 --- a/src/services/uiGraphHelper.ts +++ b/src/services/uiGraphHelper.ts @@ -16,6 +16,7 @@ import { FileEntry } from '@awesome-cordova-plugins/file'; import { UILog } from './uiLog'; import { UIFileHelper } from './uiFileHelper'; import BeanconquerorFlowTestDataDummy from '../assets/BeanconquerorFlowTestDataFourth.json'; +import { Brew } from '../classes/brew/brew'; /** * Handles every helping functionalities @@ -70,6 +71,15 @@ export class UIGraphHelper { await modal.present(); await modal.onWillDismiss(); } + public async detailBrewGraph(_brew: Brew) { + const modal = await this.modalController.create({ + component: GraphDetailComponent, + id: GraphDetailComponent.COMPONENT_ID, + componentProps: { brew: _brew }, + }); + await modal.present(); + await modal.onWillDismiss(); + } public async detailGraphRawData(_flowData: any) { const modal = await this.modalController.create({ diff --git a/src/services/uiHelper.ts b/src/services/uiHelper.ts index 51ec29d84..8026bcb1a 100755 --- a/src/services/uiHelper.ts +++ b/src/services/uiHelper.ts @@ -147,6 +147,16 @@ export class UIHelper { return moment(_unix).format(format); } + public formatTimeNumber(_time: number | string, _format?: string): string { + let format: string = + this.getSettingsStorageInstance().getSettings().date_format + + ', HH:mm:ss'; + if (_format) { + format = _format; + } + return moment(_time).format(format); + } + public toFixedIfNecessary(value, dp) { const parsedFloat = parseFloat(value); if (isNaN(parsedFloat)) { diff --git a/src/services/uiPreparationHelper.ts b/src/services/uiPreparationHelper.ts index d00acf5e8..3fd8ca616 100644 --- a/src/services/uiPreparationHelper.ts +++ b/src/services/uiPreparationHelper.ts @@ -20,9 +20,11 @@ import { PreparationDeviceType, } from '../classes/preparationDevice'; import { HttpClient } from '@angular/common/http'; -import { XeniaDevice } from '../classes/preparationDevice/xenia/xeniaDevice'; import { PreparationDevice } from '../classes/preparationDevice/preparationDevice'; import { PreparationSortToolsComponent } from '../app/preparation/preparation-sort-tools/preparation-sort-tools.component'; +import PREPARATION_TRACKING from '../data/tracking/preparationTracking'; +import { PreparationConnectedDeviceComponent } from '../app/preparation/preparation-connected-device/preparation-connected-device.component'; +import { UIAnalytics } from './uiAnalytics'; /** * Handles every helping functionalities @@ -39,7 +41,8 @@ export class UIPreparationHelper { private readonly uiHelper: UIHelper, private readonly translate: TranslateService, private readonly uiPreparationStorage: UIPreparationStorage, - private readonly httpClient: HttpClient + private readonly httpClient: HttpClient, + private readonly uiAnalytics: UIAnalytics ) { this.uiBrewStorage.attachOnEvent().subscribe((_val) => { // If an brew is deleted, we need to reset our array for the next call. @@ -84,6 +87,19 @@ export class UIPreparationHelper { await modal.present(); await modal.onWillDismiss(); } + public async connectDevice(_preparation: Preparation) { + this.uiAnalytics.trackEvent( + PREPARATION_TRACKING.TITLE, + PREPARATION_TRACKING.ACTIONS.CONNECT_DEVICE + ); + const modal = await this.modalController.create({ + component: PreparationConnectedDeviceComponent, + componentProps: { preparation: _preparation }, + id: PreparationConnectedDeviceComponent.COMPONENT_ID, + }); + await modal.present(); + await modal.onWillDismiss(); + } public async editPreparationTool( _preparation: Preparation, diff --git a/src/services/uiStorage.ts b/src/services/uiStorage.ts index 92eb75fdd..9bea4e0b1 100755 --- a/src/services/uiStorage.ts +++ b/src/services/uiStorage.ts @@ -240,13 +240,29 @@ export class UIStorage { return promise; } - public async hasCorruptedData(): Promise { - const promise: Promise = new Promise((resolve, reject) => { + public async hasCorruptedData(): Promise<{ + CORRUPTED: boolean; + DATA: { + BREWS: number; + MILL: number; + PREPARATION: number; + BEANS: number; + }; + }> { + const promise: Promise<{ + CORRUPTED: boolean; + DATA: { + BREWS: number; + MILL: number; + PREPARATION: number; + BEANS: number; + }; + }> = new Promise((resolve, reject) => { const hasDataObj = { - BREWS: false, - MILL: false, - PREPARATION: false, - BEANS: false, + BREWS: 0, + MILL: 0, + PREPARATION: 0, + BEANS: 0, }; this._storage .forEach((_value, _key, _index) => { @@ -258,7 +274,9 @@ export class UIStorage { ) { try { if (_value?.length > 0) { - hasDataObj[_key] = true; + hasDataObj[_key] = _value?.length; + } else { + hasDataObj[_key] = 0; } } catch (ex) {} } @@ -266,22 +284,22 @@ export class UIStorage { .then( () => { if ( - hasDataObj.BREWS === true && - (hasDataObj.MILL === false || - hasDataObj.PREPARATION === false || - hasDataObj.BEANS === false) + hasDataObj.BREWS > 0 && + (hasDataObj.MILL <= 0 || + hasDataObj.PREPARATION <= 0 || + hasDataObj.BEANS <= 0) ) { /** * If we got brews but not a mill / preparation / or bean something broke hard. * We saw this issue on android that a user got brews but no beans anymore, they where lost */ - resolve(true); + resolve({ CORRUPTED: true, DATA: hasDataObj }); } else { - resolve(false); + resolve({ CORRUPTED: false, DATA: hasDataObj }); } }, () => { - resolve(false); + resolve({ CORRUPTED: false, DATA: hasDataObj }); } ); }); diff --git a/src/services/uiUpdate.ts b/src/services/uiUpdate.ts index dd672e8de..60af4c65b 100755 --- a/src/services/uiUpdate.ts +++ b/src/services/uiUpdate.ts @@ -66,6 +66,7 @@ export class UIUpdate { 'UPDATE_8', 'UPDATE_9', 'UPDATE_10', + 'UPDATE_11', ]; const version: Version = this.uiVersionStorage.getVersion(); const _silentUpdate = hasData; @@ -89,6 +90,7 @@ export class UIUpdate { await this.__checkUpdateForDataVersion('UPDATE_8', !hasData); await this.__checkUpdateForDataVersion('UPDATE_9', !hasData); await this.__checkUpdateForDataVersion('UPDATE_10', !hasData); + await this.__checkUpdateForDataVersion('UPDATE_11', !hasData); } catch (ex) { if (this.uiAlert.isLoadingSpinnerShown()) { await this.uiAlert.hideLoadingSpinner(); @@ -549,8 +551,15 @@ export class UIUpdate { case 'UPDATE_10': const settings_v10: Settings = this.uiSettingsStorage.getSettings(); settings_v10.resetFilter(); + settings_v10.resetBeanSort(); await this.uiSettingsStorage.saveSettings(settings_v10); break; + case 'UPDATE_11': + //#776 - We added a new beansort, thats why we reset the bean sort here. + const settings_v11: Settings = this.uiSettingsStorage.getSettings(); + settings_v11.resetBeanSort(); + await this.uiSettingsStorage.saveSettings(settings_v11); + break; default: break; }