diff --git a/client/package.json b/client/package.json index 11c10e5..85806ec 100644 --- a/client/package.json +++ b/client/package.json @@ -12,7 +12,7 @@ }, "private": true, "dependencies": { - "@angular-mdl/core": "^4.0.3", + "@angular-mdl/core": "^4.0.5", "@angular/animations": "^4.1.3", "@angular/common": "^4.1.3", "@angular/compiler": "^4.1.3", @@ -22,21 +22,22 @@ "@angular/platform-browser": "^4.1.3", "@angular/platform-browser-dynamic": "^4.1.3", "@angular/router": "^4.1.3", - "@swimlane/ngx-datatable": "^9.1.0", + "@swimlane/ngx-datatable": "^9.2.0", "core-js": "^2.4.1", "nedb": "^1.8.0", + "ngx-electron": "0.0.11", "rxjs": "^5.4.0", - "typescript": "^2.3.2", - "zone.js": "^0.8.10" + "typescript": "^2.3.3", + "zone.js": "^0.8.11" }, "devDependencies": { - "@angular/cli": "^1.0.3", + "@angular/cli": "^1.0.6", "@angular/compiler-cli": "^4.1.3", "@types/nedb": "^1.8.3", - "@types/node": "~7.0.18", + "@types/node": "~7.0.22", "codelyzer": "~3.0.1", "concurrently": "^3.4.0", "ts-node": "~3.0.4", - "tslint": "~5.2.0" + "tslint": "~5.3.2" } } diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 1246ff3..b716930 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -1,10 +1,10 @@ import { Component } from '@angular/core'; - +import { UpdaterService } from './updater.service'; @Component({ selector: 'pokemon-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { - + constructor(private updaterService: UpdaterService) { } } diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index f272682..7d8196c 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -13,15 +13,23 @@ import { CardService } from './card.service'; import { CollectionService } from './collection.service'; import { MenuItemComponent } from './menu-item/menu-item.component'; +import { UpdaterService } from './updater.service'; + import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { NgxElectronModule } from 'ngx-electron'; + +import { UpdateAvailableDialogComponent } from './dialog/update-available-dialog.component'; + @NgModule({ declarations: [ AppComponent, HomeComponent, - MenuItemComponent + MenuItemComponent, + UpdateAvailableDialogComponent ], imports: [ + NgxElectronModule, BrowserModule, HttpModule, BrowserAnimationsModule, @@ -29,10 +37,14 @@ import { NgxDatatableModule } from '@swimlane/ngx-datatable'; NgxDatatableModule ], providers: [ + UpdaterService, SetService, CardService, CollectionService ], - bootstrap: [AppComponent] + bootstrap: [AppComponent], + entryComponents: [ + UpdateAvailableDialogComponent + ] }) export class AppModule { } diff --git a/client/src/app/card.service.ts b/client/src/app/card.service.ts index c18b245..a6b19e0 100644 --- a/client/src/app/card.service.ts +++ b/client/src/app/card.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { Http } from '@angular/http'; -import { Observable } from 'rxjs'; +import { Observable } from 'rxjs/observable'; import * as Datastore from 'nedb'; import { Card } from './models/card.interface'; @@ -11,14 +11,14 @@ import { CardStore } from './database/card.store'; @Injectable() export class CardService { - private cardStore: CardStore; + private cardStore: CardStore; constructor(private http: Http) { - let db = new Datastore({ filename: 'cards.db', autoload: true }); + const db: Datastore = new Datastore({ filename: 'cards.db', autoload: true }); this.cardStore = new CardStore(db, http); } public get(set: Set): Observable { return this.cardStore.getCards(set); } -} \ No newline at end of file +} diff --git a/client/src/app/collection.service.ts b/client/src/app/collection.service.ts index 23430bb..7626d6f 100644 --- a/client/src/app/collection.service.ts +++ b/client/src/app/collection.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Observable } from 'rxjs/observable'; import * as Datastore from 'nedb'; import { Collection } from './models/collection.interface'; @@ -13,7 +13,7 @@ export class CollectionService { private collectionStore: CollectionStore; constructor() { - let db = new Datastore({ filename: 'collection.db', autoload: true }); + const db: Datastore = new Datastore({ filename: 'collection.db', autoload: true }); this.collectionStore = new CollectionStore(db); } @@ -28,4 +28,4 @@ export class CollectionService { public countCollected(setCode: string): Observable { return this.collectionStore.countCollectedSet(setCode); } -} \ No newline at end of file +} diff --git a/client/src/app/database/card.store.ts b/client/src/app/database/card.store.ts index bce5825..cb014fa 100644 --- a/client/src/app/database/card.store.ts +++ b/client/src/app/database/card.store.ts @@ -2,9 +2,11 @@ import { Card } from '../models/card.interface'; import { Set } from '../models/set.interface'; import { Http, Response, URLSearchParams } from '@angular/http'; -import { Observable, Subject } from 'rxjs'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; import * as Datastore from 'nedb'; + /** * Performs CRUD on the NEDB data store that is passed to it in the constructor. */ @@ -20,8 +22,8 @@ export class CardStore { */ public getCards(set: Set): Observable { this.db.find({ setCode: set.code }).exec((err, dbcards) => { - if (dbcards.length === 0 || dbcards.length != set.totalCards) { - let params = new URLSearchParams(); + if (dbcards.length === 0 || dbcards.length !== set.totalCards) { + const params: URLSearchParams = new URLSearchParams(); params.append('setCode', set.code); params.append('pageSize', String(set.totalCards)); this.http.get('https://api.pokemontcg.io/v1/cards', { search: params }) @@ -34,7 +36,7 @@ export class CardStore { this.cardListSubject.next(dbcards); } }); - + return this.cardListSubject; } -} \ No newline at end of file +} diff --git a/client/src/app/database/collection.store.ts b/client/src/app/database/collection.store.ts index 1c0ec5a..cb9db9b 100644 --- a/client/src/app/database/collection.store.ts +++ b/client/src/app/database/collection.store.ts @@ -1,8 +1,8 @@ -import { Observable } from 'rxjs'; - import { Collection } from '../models/collection.interface'; -import { Subject, BehaviorSubject } from 'rxjs'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import * as Datastore from 'nedb'; @@ -10,7 +10,7 @@ import * as Datastore from 'nedb'; * Performs CRUD on the NEDB data store that is passed to it in the constructor. */ export class CollectionStore { - private countCollectedSubject: codeToObservable = {}; + private countCollectedSubject: CodeToObservable = {}; constructor( private db: Datastore) { } @@ -24,7 +24,7 @@ export class CollectionStore { } public getCollection(setCode): Observable { - let collectionSubject = new Subject(); + const collectionSubject: Subject = new Subject(); this.db.find({ setCode: setCode }).exec((err, collection) => { collectionSubject.next(collection); @@ -34,9 +34,10 @@ export class CollectionStore { } public collectCard(card): Observable { - let cardCollectedSubject = new Subject(); + const cardCollectedSubject: Subject = new Subject(); + this.db.insert(new Collection(card.id, card.setCode), (err, collection) => { - cardCollectedSubject.next(Collection); + cardCollectedSubject.next(collection); this.countSetCollection(card.setCode); }); @@ -51,6 +52,6 @@ export class CollectionStore { } } -interface codeToObservable { +interface CodeToObservable { [setCode: string]: BehaviorSubject; -} \ No newline at end of file +} diff --git a/client/src/app/database/set.store.ts b/client/src/app/database/set.store.ts index 13ad4c6..bef72f2 100644 --- a/client/src/app/database/set.store.ts +++ b/client/src/app/database/set.store.ts @@ -1,7 +1,8 @@ import { Set } from '../models/set.interface'; import { Http, Response } from '@angular/http'; -import { Observable, Subject } from 'rxjs'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; import * as Datastore from 'nedb'; /** @@ -30,7 +31,7 @@ export class SetStore { this.setListSubject.next(dbsets); } }); - + return this.setListSubject; } -} \ No newline at end of file +} diff --git a/client/src/app/dialog/update-available-dialog.component.css b/client/src/app/dialog/update-available-dialog.component.css new file mode 100644 index 0000000..bb78556 --- /dev/null +++ b/client/src/app/dialog/update-available-dialog.component.css @@ -0,0 +1,3 @@ +h4 { + margin-top: 0; +} \ No newline at end of file diff --git a/client/src/app/dialog/update-available-dialog.component.html b/client/src/app/dialog/update-available-dialog.component.html new file mode 100644 index 0000000..84b67ce --- /dev/null +++ b/client/src/app/dialog/update-available-dialog.component.html @@ -0,0 +1,12 @@ +
+

Version {{ updateInfo.version }} is now available

+
+

Release notes:

+

+
+
+ + + +
+
diff --git a/client/src/app/dialog/update-available-dialog.component.ts b/client/src/app/dialog/update-available-dialog.component.ts new file mode 100644 index 0000000..65db395 --- /dev/null +++ b/client/src/app/dialog/update-available-dialog.component.ts @@ -0,0 +1,28 @@ +import { Component, Inject } from '@angular/core'; +import { MdlDialogReference } from '@angular-mdl/core'; + +import { UpdateInfo } from '../models/update-info.interface'; +import { UPDATE_INFO } from '../models/update-info.token'; + +@Component({ + selector: 'pokemon-update-available-dialog-component', + templateUrl: './update-available-dialog.component.html', + styleUrls: ['./update-available-dialog.component.css'] +}) +export class UpdateAvailableDialogComponent { + public updateInfo: UpdateInfo; + + constructor( + @Inject(UPDATE_INFO) updateInfo: UpdateInfo, + private dialog: MdlDialogReference) { + this.updateInfo = updateInfo; + } + + public dismissUpdate(): void { + this.dialog.hide(false); + } + + public doUpdate(): void { + this.dialog.hide(true); + } +} diff --git a/client/src/app/home/home.component.html b/client/src/app/home/home.component.html index 94ab27a..19a6a25 100644 --- a/client/src/app/home/home.component.html +++ b/client/src/app/home/home.component.html @@ -8,30 +8,30 @@ Sets - - + + class='material' + [columnMode]='"force"' + [rowHeight]='"auto"' + [rows]='cards' + [columns]='columns' + [rowClass]='getRowClass' + [messages]='{ emptyMessage: "No set selected" }'> - - -
{{ value | date : 'mediumDate' }}
+
{{ value | date : 'mediumDate' }}
diff --git a/client/src/app/home/home.component.ts b/client/src/app/home/home.component.ts index 402ee1f..ca78f37 100644 --- a/client/src/app/home/home.component.ts +++ b/client/src/app/home/home.component.ts @@ -6,7 +6,9 @@ import { SetService } from '../set.service'; import { CardService } from '../card.service'; import { CollectionService } from '../collection.service'; -import { Observable } from 'rxjs'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/observable/zip'; import { Set } from '../models/set.interface'; import { Card } from '../models/card.interface'; import { Collection } from '../models/collection.interface'; @@ -51,8 +53,8 @@ export class HomeComponent implements OnInit { } dateComparator(dateA: Date, dateB: Date) { - if (dateB < dateA) return 1; - else if (dateB > dateA) return -1; + if (dateB < dateA) { return 1; } + if (dateB > dateA) { return -1; } return 0; } @@ -60,8 +62,8 @@ export class HomeComponent implements OnInit { this.sets = this.setService.getSetList() .map((sets: Set[]) => { return sets.sort((a: Set, b: Set) => { - if (new Date(b.releaseDate) < new Date(a.releaseDate)) return 1; - else if (new Date(b.releaseDate) > new Date(a.releaseDate)) return -1; + if (new Date(b.releaseDate) < new Date(a.releaseDate)) { return 1; } + if (new Date(b.releaseDate) > new Date(a.releaseDate)) { return -1; } return 0; }); }); @@ -96,12 +98,12 @@ export class HomeComponent implements OnInit { .subscribe(([cards, collection]) => { this.cards = cards; collection.forEach(collectedEntry => { - var foundCard = cards.find(function (card) { - return card.id == collectedEntry.card; + const foundCard: Card = cards.find(function (card) { + return card.id === collectedEntry.card; }); foundCard.collected = collectedEntry.collected; }); }); } -} \ No newline at end of file +} diff --git a/client/src/app/menu-item/menu-item.component.css b/client/src/app/menu-item/menu-item.component.css index 328d455..d74b40f 100644 --- a/client/src/app/menu-item/menu-item.component.css +++ b/client/src/app/menu-item/menu-item.component.css @@ -1,7 +1,7 @@ .set.active { - background-color: lightgray; + background-color: lightgray; } .set:hover { - background-color: lightslategray; + background-color: lightslategray; } \ No newline at end of file diff --git a/client/src/app/menu-item/menu-item.component.html b/client/src/app/menu-item/menu-item.component.html index 1b137bd..df8a9a5 100644 --- a/client/src/app/menu-item/menu-item.component.html +++ b/client/src/app/menu-item/menu-item.component.html @@ -1,7 +1,7 @@ - - {{ set.name }} - {{ set.releaseDate }} - - {{ count / set.totalCards | percent:'1.0-2' }} + + {{ set.name }} + {{ set.releaseDate }} + + {{ count / set.totalCards | percent:'1.0-2' }} \ No newline at end of file diff --git a/client/src/app/menu-item/menu-item.component.ts b/client/src/app/menu-item/menu-item.component.ts index 284cd10..f38ace3 100644 --- a/client/src/app/menu-item/menu-item.component.ts +++ b/client/src/app/menu-item/menu-item.component.ts @@ -14,18 +14,18 @@ export class MenuItemComponent implements AfterViewInit { @Input() selectedSet: string; @Output() selectSet = new EventEmitter(); - public count: number = 0; + public count = 0; constructor(private collectionService: CollectionService) { } ngAfterViewInit(): void { - this.collectionService.countCollected(this.set.code) + this.collectionService.countCollected(this.set.code) .subscribe(count => { - this.count = count; - }); + this.count = count; + }); } select() { this.selectSet.emit(this.set); } -} \ No newline at end of file +} diff --git a/client/src/app/models/card.interface.ts b/client/src/app/models/card.interface.ts index d3d8aee..67781d1 100644 --- a/client/src/app/models/card.interface.ts +++ b/client/src/app/models/card.interface.ts @@ -6,4 +6,4 @@ export interface Card { series: string; setCode: string; collected: Date; -} \ No newline at end of file +} diff --git a/client/src/app/models/collection.interface.ts b/client/src/app/models/collection.interface.ts index 1b8ae47..6363cd1 100644 --- a/client/src/app/models/collection.interface.ts +++ b/client/src/app/models/collection.interface.ts @@ -8,4 +8,4 @@ export class Collection { this.setCode = set; this.collected = new Date(); } -} \ No newline at end of file +} diff --git a/client/src/app/models/set.interface.ts b/client/src/app/models/set.interface.ts index cf8fda7..765cb16 100644 --- a/client/src/app/models/set.interface.ts +++ b/client/src/app/models/set.interface.ts @@ -1,8 +1,8 @@ export interface Set { - name: string; - series: string; - totalCards: number; - code: string; + name: string; + series: string; + totalCards: number; + code: string; releaseDate: string; collectedCount: number; -} \ No newline at end of file +} diff --git a/client/src/app/models/update-info.interface.ts b/client/src/app/models/update-info.interface.ts new file mode 100644 index 0000000..e7ed9ce --- /dev/null +++ b/client/src/app/models/update-info.interface.ts @@ -0,0 +1,4 @@ +export interface UpdateInfo { + releaseNotes: string; + version: string; +} diff --git a/client/src/app/models/update-info.token.ts b/client/src/app/models/update-info.token.ts new file mode 100644 index 0000000..0ebc6ed --- /dev/null +++ b/client/src/app/models/update-info.token.ts @@ -0,0 +1,4 @@ +import { InjectionToken } from '@angular/core'; +import { UpdateInfo } from './update-info.interface'; + +export let UPDATE_INFO = new InjectionToken('release.info'); diff --git a/client/src/app/set.service.ts b/client/src/app/set.service.ts index 116335a..6da7458 100644 --- a/client/src/app/set.service.ts +++ b/client/src/app/set.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { Http } from '@angular/http'; -import { Observable } from 'rxjs'; +import { Observable } from 'rxjs/Observable'; import * as Datastore from 'nedb'; import { Set } from './models/set.interface'; @@ -10,14 +10,14 @@ import { SetStore } from './database/set.store'; @Injectable() export class SetService { - private setStore: SetStore; + private setStore: SetStore; - constructor(private http: Http) { - let db = new Datastore({ filename: 'sets.db', autoload: true }); - this.setStore = new SetStore(db, http); - } + constructor(private http: Http) { + const db: Datastore = new Datastore({ filename: 'sets.db', autoload: true }); + this.setStore = new SetStore(db, http); + } - public getSetList(): Observable { - return this.setStore.getSets(); - } -} \ No newline at end of file + public getSetList(): Observable { + return this.setStore.getSets(); + } +} diff --git a/client/src/app/updater.service.ts b/client/src/app/updater.service.ts new file mode 100644 index 0000000..e77da12 --- /dev/null +++ b/client/src/app/updater.service.ts @@ -0,0 +1,96 @@ +import { Injectable, NgZone } from '@angular/core'; +import { MdlSnackbarService, MdlDialogService, MdlDialogReference } from '@angular-mdl/core'; + +import * as Datastore from 'nedb'; + +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + +import { ElectronService } from 'ngx-electron'; +import { UpdateAvailableDialogComponent } from './dialog/update-available-dialog.component'; + +import { UpdateInfo } from './models/update-info.interface'; +import { UPDATE_INFO } from './models/update-info.token'; + +@Injectable() +export class UpdaterService { + private dialogObservable: Subject; + + constructor( + private electronService: ElectronService, + private mdlSnackbarService: MdlSnackbarService, + private mdlDialogService: MdlDialogService, + private ngZone: NgZone + ) { + this.setupUpToDateHandler(); + this.setupNewVersionHandler(); + this.setupDownloadStartedHandler(); + this.setupDownloadFinishedHandler(); + // this.setupDownloadProgressHandler(); + + electronService.ipcRenderer.send('check-update'); + } + + private setupNewVersionHandler(): void { + this.electronService.ipcRenderer.on('update-available', (ev, info: UpdateInfo) => { + this.ngZone.run(() => { + const updateAvailableDialog: Observable = this.mdlDialogService.showCustomDialog({ + component: UpdateAvailableDialogComponent, + providers: [ + { + provide: UPDATE_INFO, + useValue: info + } + ], + isModal: true, + styles: { 'width': '650px' }, + clickOutsideToClose: false, + enterTransitionDuration: 400, + leaveTransitionDuration: 400 + }); + + updateAvailableDialog.subscribe((dialogRef: MdlDialogReference) => { + dialogRef.onHide().subscribe((data) => { + if (data) { + this.electronService.ipcRenderer.send('download-update'); + } + }); + }); + }); + }); + } + + private setupUpToDateHandler(): void { + this.electronService.ipcRenderer.on('up-to-date', (event) => { + this.ngZone.run(() => { + this.mdlSnackbarService.showToast('Hooray, you\'re using the latest version!'); + }); + }); + } + + private setupDownloadStartedHandler(): void { + this.electronService.ipcRenderer.on('update-download-started', () => { + this.ngZone.run(() => { + this.mdlSnackbarService.showToast('Update downloading, you will be prompted when this is finished'); + }); + }); + } + + private setupDownloadFinishedHandler(): void { + this.electronService.ipcRenderer.on('update-download-finished', () => { + this.ngZone.run(() => { + const updateDownloadedDialog: Observable = + this.mdlDialogService.alert('Update downloaded, application will close to install now', 'Ok', 'Update downloaded'); + updateDownloadedDialog.subscribe(() => { + this.electronService.ipcRenderer.send('install-update'); + }); + }); + }); + } + + private setupDownloadProgressHandler(): void { + this.electronService.ipcRenderer.on('update-download-progress', (event, progress) => { + console.log(progress); + }); + } +} diff --git a/client/src/index.html b/client/src/index.html index 986a44d..d9d15af 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -1,14 +1,14 @@ - + Pokemon TCG Tracker - - + Loading... + diff --git a/electron/updater.js b/electron/updater.js index 5f69f2e..c917b51 100644 --- a/electron/updater.js +++ b/electron/updater.js @@ -1,42 +1,57 @@ -const { autoUpdater } = require("electron-updater"); -const { dialog } = require('electron'); +const { autoUpdater } = require('electron-updater'); +const { dialog, BrowserWindow, ipcMain } = require('electron'); autoUpdater.autoDownload = false; + /** * Autoupdater events */ -autoUpdater.on('update-available', () => { - dialog.showMessageBox({ - type: 'info', - title: 'Found Updates', - message: 'Found updates, do you want update now?', - buttons: ['Yes', 'No'] - }, (buttonIndex) => { - if (buttonIndex === 0) { - autoUpdater.downloadUpdate() - } - }) +autoUpdater.on('update-available', (info) => { + notify('update-available', info); +}); + +autoUpdater.on('update-not-available', () => { + notify('up-to-date'); +}); + +autoUpdater.on('download-progress', (progress) => { + notify('update-download-progress', progress); }); autoUpdater.on('update-downloaded', (event, info) => { - dialog.showMessageBox({ - title: 'Update downloaded', - message: 'New version downloaded, installing now' - }, (buttonIndex) => { - autoUpdater.quitAndInstall(); - }); + notify('update-download-finished'); }); -/* -autoUpdater.on('update-not-available', () => { - dialog.showMessageBox({ - title: 'No Updates', - message: 'Current version is up-to-date.' - }) -})*/ + autoUpdater.on('error', (event, error) => { - dialog.showErrorBox('Error: ', error == null ? "unknown" : (error.stack || error).toString()) + dialog.showErrorBox('Error: ', error == null ? 'unknown' : (error.stack || error).toString()) }) -autoUpdater.checkForUpdates(); \ No newline at end of file + +/** + * IPC events + */ +ipcMain.on('check-update', () => { + autoUpdater.checkForUpdates(); + + notify('check-update-started'); +}); + +ipcMain.on('download-update', () => { + autoUpdater.downloadUpdate(); + + notify('update-download-started'); +}); + +ipcMain.on('install-update', () => { + notify('install-update-starting'); + + autoUpdater.quitAndInstall(); +}); + +function notify(title, message) { + let windows = BrowserWindow.getAllWindows(); + + windows[0].webContents.send(title, message); +} \ No newline at end of file diff --git a/main.js b/main.js index 8686f47..3832dbb 100644 --- a/main.js +++ b/main.js @@ -14,7 +14,7 @@ function createWindow () { // Make sure content is always shown on big screen, never collapsed together // Windows adds the scrollbar inside of the window - width = (process.platform === "darwin" ? 1025 : 1041); + width = (process.platform === 'darwin' ? 1025 : 1041); mainWindow = new BrowserWindow({ width: width, minWidth: width, diff --git a/package.json b/package.json index 82302b8..5d0091b 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "start": "concurrently \"electron .\" \"cd client && ng serve\" ", "build": "ng build", "compile": "cd client && ng build --base-href . --prod -op ../build_client", + "test-release": "npm run compile && npm run dist:win", "release": "npm run compile && build --publish onTagOrDraft" }, "repository": { @@ -28,9 +29,9 @@ "js-yaml": "^3.8.4" }, "devDependencies": { - "electron": "^1.6.8", + "electron": "^1.6.10", "concurrently": "^3.4.0", - "electron-builder": "^17.8.0" + "electron-builder": "^18.0.1" }, "build": { "appId": "pokemon.tcg.tracker",