From 0a9731019396c0a2b778deea508b7926c70692f6 Mon Sep 17 00:00:00 2001 From: hokiepokedad2 <38219945+hokiepokedad2@users.noreply.github.com> Date: Wed, 19 Aug 2026 08:46:23 -0400 Subject: [PATCH 1/3] feat(where): default place for new alarms, and mega PVP Two remaining pieces of per-alarm scope. Default place - AlertDefaultsService remembers a place beside the mode and radius, and the nine add dialogs that lacked it gain the "measured from" selector Pokemon already had. All ten seed from the saved default. - save() drops the place when the mode is areas. A place only means something alongside a radius, and the two are mutually exclusive upstream, so keeping one would seed every new alarm with a scope PoracleNG refuses. - reconcilePlace() forgets a place that no longer exists, so deleting one cannot keep stamping a rejected label onto new alarms. Mega PVP - pvp_ranking_evolution on the Monster model, a Base / Mega / Mega X / Mega Y control in the add and edit dialogs, and a suffix on the card's PVP badge. - Only sent when the rule has a league. Carrying it on a non-PVP alarm would be a filter nobody asked for on a field PoracleNG still reads. - It works whatever include_mega_evolution is set to: that flag only decides the default for rules that do not state a mode. Confirmed against 5.1.0's matcher, where filterMega is gone and mega entries are split per evolution. Translations for all new strings in the ten non-English locales. Two rounds of build-only failures again, both invisible to tsc and jest: mat-option unknown in three dialogs whose imports lacked MatSelectModule, and a spec stub for AlertDefaultsService missing the new accessor. --- .../ClientApp/src/app/core/models/index.ts | 1 + .../services/alert-defaults.service.spec.ts | 29 ++++++++++++++++ .../core/services/alert-defaults.service.ts | 27 ++++++++++++++- .../fort-change-add-dialog.component.html | 21 +++++++++--- .../fort-change-add-dialog.component.scss | 6 ++++ .../fort-change-add-dialog.component.ts | 18 ++++++++-- .../gyms/gym-add-dialog.component.html | 23 +++++++++---- .../gyms/gym-add-dialog.component.scss | 6 ++++ .../modules/gyms/gym-add-dialog.component.ts | 20 +++++++++-- .../invasion-add-dialog.component.html | 23 +++++++++---- .../invasion-add-dialog.component.scss | 6 ++++ .../invasion-add-dialog.component.spec.ts | 5 ++- .../invasion-add-dialog.component.ts | 21 ++++++++++-- .../lures/lure-add-dialog.component.html | 23 +++++++++---- .../lures/lure-add-dialog.component.scss | 6 ++++ .../lures/lure-add-dialog.component.ts | 21 ++++++++++-- .../max-battle-add-dialog.component.html | 21 +++++++++--- .../max-battle-add-dialog.component.scss | 6 ++++ .../max-battle-add-dialog.component.ts | 23 ++++++++++--- .../nests/nest-add-dialog.component.html | 23 +++++++++---- .../nests/nest-add-dialog.component.scss | 6 ++++ .../nests/nest-add-dialog.component.ts | 20 +++++++++-- .../pokemon/pokemon-add-dialog.component.html | 13 +++++++ .../pokemon-add-dialog.component.spec.ts | 26 +++++++++++++- .../pokemon/pokemon-add-dialog.component.ts | 3 ++ .../pokemon-edit-dialog.component.html | 10 ++++++ .../pokemon/pokemon-edit-dialog.component.ts | 3 ++ .../pokemon/pokemon-list.component.html | 3 ++ .../modules/pokemon/pokemon-list.component.ts | 14 ++++++++ .../quests/quest-add-dialog.component.html | 21 +++++++++--- .../quests/quest-add-dialog.component.scss | 6 ++++ .../quests/quest-add-dialog.component.ts | 34 +++++++++++++++---- .../raids/raid-add-dialog.component.html | 21 +++++++++--- .../raids/raid-add-dialog.component.scss | 6 ++++ .../raids/raid-add-dialog.component.ts | 27 ++++++++++++--- .../alert-defaults-dialog.component.html | 10 ++++++ .../alert-defaults-dialog.component.ts | 29 +++++++++++++--- .../ClientApp/src/assets/i18n/da.json | 6 ++++ .../ClientApp/src/assets/i18n/de.json | 6 ++++ .../ClientApp/src/assets/i18n/en.json | 6 ++++ .../ClientApp/src/assets/i18n/es.json | 6 ++++ .../ClientApp/src/assets/i18n/fr.json | 6 ++++ .../ClientApp/src/assets/i18n/it.json | 6 ++++ .../ClientApp/src/assets/i18n/nl.json | 6 ++++ .../ClientApp/src/assets/i18n/pl.json | 6 ++++ .../ClientApp/src/assets/i18n/pt-BR.json | 6 ++++ .../ClientApp/src/assets/i18n/pt.json | 6 ++++ .../ClientApp/src/assets/i18n/sv.json | 6 ++++ CHANGELOG.md | 3 ++ 49 files changed, 572 insertions(+), 78 deletions(-) diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/models/index.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/models/index.ts index 1d538c87..18fa5737 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/models/index.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/models/index.ts @@ -31,6 +31,7 @@ export interface Monster { profileNo: number; pvpRankingBest: number; pvpRankingCap: number; + pvpRankingEvolution: number; pvpRankingLeague: number; pvpRankingMinCp: number; pvpRankingWorst: number; diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/services/alert-defaults.service.spec.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/services/alert-defaults.service.spec.ts index 4657f289..1ac42251 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/services/alert-defaults.service.spec.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/services/alert-defaults.service.spec.ts @@ -54,4 +54,33 @@ describe('AlertDefaultsService', () => { service.save('distance', NaN); expect(service.defaultDistanceKm()).toBe(1); }); + it('remembers a default place alongside a distance', () => { + const service = new AlertDefaultsService(); + service.save('distance', 2, 'work'); + expect(service.defaultPlaceLabel()).toBe('work'); + expect(new AlertDefaultsService().defaultPlaceLabel()).toBe('work'); + }); + + it('drops the place when the default is areas', () => { + // A place only means anything alongside a radius; the two are mutually exclusive upstream. Keeping + // one here would seed every new alarm with a scope PoracleNG refuses. + const service = new AlertDefaultsService(); + service.save('distance', 2, 'work'); + service.save('areas', 2, 'work'); + expect(service.defaultPlaceLabel()).toBe(''); + }); + + it('forgets a default place that no longer exists', () => { + const service = new AlertDefaultsService(); + service.save('distance', 2, 'work'); + service.reconcilePlace(['home']); + expect(service.defaultPlaceLabel()).toBe(''); + }); + + it('keeps a default place that still exists', () => { + const service = new AlertDefaultsService(); + service.save('distance', 2, 'work'); + service.reconcilePlace(['home', 'work']); + expect(service.defaultPlaceLabel()).toBe('work'); + }); }); diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/services/alert-defaults.service.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/services/alert-defaults.service.ts index 261b1bca..7a748e27 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/services/alert-defaults.service.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/core/services/alert-defaults.service.ts @@ -11,6 +11,7 @@ export const MAX_DEFAULT_DISTANCE_KM = 100; const MODE_KEY = 'poracle-default-alert-mode'; const DISTANCE_KEY = 'poracle-default-alert-distance-km'; +const PLACE_KEY = 'poracle-default-alert-place'; /** * Client-side preference for how brand-new alarms default their delivery scope. @@ -29,13 +30,37 @@ export class AlertDefaultsService { /** Preferred location mode used to seed new alarms. */ readonly defaultMode = signal(this.readMode()); + /** + * Saved place new distance-mode alarms measure from. Empty means the profile pin, which is what + * every alarm did before per-alarm scope existed. + */ + readonly defaultPlaceLabel = signal(localStorage.getItem(PLACE_KEY) ?? ''); + + /** + * Drops the remembered place when it no longer exists, so a deleted place cannot keep seeding new + * alarms with a label PoracleNG will reject. + */ + reconcilePlace(known: string[]): void { + const current = this.defaultPlaceLabel(); + if (current && !known.includes(current)) { + this.defaultPlaceLabel.set(''); + localStorage.setItem(PLACE_KEY, ''); + } + } + /** Persist the user's preferred defaults for new alarms. */ - save(mode: AlertLocationMode, distanceKm: number): void { + save(mode: AlertLocationMode, distanceKm: number, placeLabel = ''): void { const km = this.clampDistance(distanceKm); + // A place only means something alongside a radius. Keeping one on the areas default would seed + // every new alarm with a scope PoracleNG refuses. + const place = mode === 'distance' ? placeLabel : ''; + this.defaultMode.set(mode); this.defaultDistanceKm.set(km); + this.defaultPlaceLabel.set(place); localStorage.setItem(MODE_KEY, mode); localStorage.setItem(DISTANCE_KEY, String(km)); + localStorage.setItem(PLACE_KEY, place); } private clampDistance(value: number): number { diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/fort-changes/fort-change-add-dialog.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/fort-changes/fort-change-add-dialog.component.html index ad740548..34da3f1e 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/fort-changes/fort-change-add-dialog.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/fort-changes/fort-change-add-dialog.component.html @@ -61,11 +61,22 @@

{{ 'ALARM.LOCATION_MODE' | translate }}

@if (form.controls.distanceMode.value === 'distance') { - - {{ 'ALARM.DISTANCE_LABEL' | translate }} - - {{ 'ALARM.DISTANCE_SUFFIX' | translate }} - +
+ + {{ 'WHERE.MEASURED_FROM' | translate }} + + {{ 'WHERE.MY_PIN' | translate }} + @for (place of places.named(); track place.label) { + {{ place.label }} + } + + + + {{ 'ALARM.DISTANCE_LABEL' | translate }} + + {{ 'ALARM.DISTANCE_SUFFIX' | translate }} + +
} {{ 'ALARM.LOCATION_MODE' | translate }} > @if (form.controls.distanceMode.value === 'distance') { - {{ 'ALARM.DISTANCE_LABEL' | translate }}{{ - 'ALARM.DISTANCE_SUFFIX' | translate - }} +
+ + {{ 'WHERE.MEASURED_FROM' | translate }} + + {{ 'WHERE.MY_PIN' | translate }} + @for (place of places.named(); track place.label) { + {{ place.label }} + } + + + {{ 'ALARM.DISTANCE_LABEL' | translate }}{{ + 'ALARM.DISTANCE_SUFFIX' | translate + }} +
} (null); selectedTeamIds = signal([]); @@ -84,6 +92,7 @@ export class GymAddDialogComponent { } onDistanceModeChange(): void { + if (this.form.controls.distanceMode.value === 'areas') this.form.controls.placeLabel.setValue(''); if (this.form.controls.distanceMode.value === 'areas') this.form.controls.distanceKm.setValue(0); else if (!this.form.controls.distanceKm.value) this.form.controls.distanceKm.setValue(1); } @@ -92,12 +101,19 @@ export class GymAddDialogComponent { if (this.selectedTeamIds().length === 0) return; this.saving.set(true); const v = this.form.getRawValue(); - const dist = v.distanceMode === 'areas' ? 0 : Math.round((v.distanceKm ?? 1) * 1000); + // One conversion for all three answers, shared with the card chip and the scope sheet. + const scope = scopeToFields( + v.distanceMode === 'areas' + ? { mode: 'profile' } + : { distanceKm: v.distanceKm ?? 1, mode: v.placeLabel ? 'place' : 'profile', placeLabel: v.placeLabel ?? '' }, + ); const creates = this.selectedTeamIds().map(team => this.gymService.create({ + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, battleChanges: v.battleChanges ? 1 : 0, clean: compose(!!v.clean, false, false), - distance: dist, + distance: scope.distance, gymId: this.selectedGymId() ?? '', slotChanges: v.slotChanges ? 1 : 0, team, diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/invasions/invasion-add-dialog.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/invasions/invasion-add-dialog.component.html index c72ec14b..a8f3cd0d 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/invasions/invasion-add-dialog.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/invasions/invasion-add-dialog.component.html @@ -85,12 +85,23 @@

{{ 'ALARM.LOCATION_MODE' | translate }}

> @if (form.controls.distanceMode.value === 'distance') { - {{ 'ALARM.DISTANCE_LABEL' | translate }}{{ - 'ALARM.DISTANCE_SUFFIX' | translate - }} +
+ + {{ 'WHERE.MEASURED_FROM' | translate }} + + {{ 'WHERE.MY_PIN' | translate }} + @for (place of places.named(); track place.label) { + {{ place.label }} + } + + + {{ 'ALARM.DISTANCE_LABEL' | translate }}{{ + 'ALARM.DISTANCE_SUFFIX' | translate + }} +
} { { provide: I18nService, useValue: { instant: (k: string) => k } }, { provide: AuthService, useValue: { isImpersonating: () => false } }, { provide: MasterDataService, useValue: { getPokemon: () => of([]) } }, - { provide: AlertDefaultsService, useValue: { defaultDistanceKm: () => 1, defaultMode: () => 'areas' } }, + { + provide: AlertDefaultsService, + useValue: { defaultDistanceKm: () => 1, defaultMode: () => 'areas', defaultPlaceLabel: () => '' }, + }, { provide: InvasionService, useValue: { diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/invasions/invasion-add-dialog.component.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/invasions/invasion-add-dialog.component.ts index bd16d604..0d25899a 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/invasions/invasion-add-dialog.component.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/invasions/invasion-add-dialog.component.ts @@ -20,8 +20,10 @@ import { AuthService } from '../../core/services/auth.service'; import { I18nService } from '../../core/services/i18n.service'; import { InvasionService } from '../../core/services/invasion.service'; import { MasterDataService } from '../../core/services/masterdata.service'; +import { PlacesService } from '../../core/services/places.service'; import { DeliveryPreviewComponent } from '../../shared/components/delivery-preview/delivery-preview.component'; import { TemplateSelectorComponent } from '../../shared/components/template-selector/template-selector.component'; +import { scopeToFields } from '../../shared/utils/alarm-scope'; interface GruntOption { color?: string; @@ -113,19 +115,23 @@ export class InvasionAddDialogComponent implements OnInit { private readonly masterData = inject(MasterDataService); private readonly snackBar = inject(MatSnackBar); readonly dialogRef = inject(MatDialogRef); - gruntOptions = signal([]); readonly eventGrunts = computed(() => this.gruntOptions().filter(g => g.isEvent)); + form = this.fb.group({ clean: [false], distanceKm: [this.alertDefaults.defaultDistanceKm()], distanceMode: [this.alertDefaults.defaultMode()], gender: [0], + // Empty means the profile pin, which is what a radius has always meant. + placeLabel: [this.alertDefaults.defaultPlaceLabel()], template: [''], }); readonly isWebhook = inject(AuthService).isImpersonating(); + + readonly places = inject(PlacesService); readonly rocketGrunts = computed(() => this.gruntOptions().filter(g => !g.isEvent)); saving = signal(false); @@ -157,6 +163,7 @@ export class InvasionAddDialogComponent implements OnInit { } ngOnInit(): void { + this.places.load().subscribe({ error: () => undefined }); const grunts: GruntOption[] = InvasionAddDialogComponent.GRUNT_TYPES.map(g => ({ ...g, isEvent: false, @@ -174,6 +181,7 @@ export class InvasionAddDialogComponent implements OnInit { } onDistanceModeChange(): void { + if (this.form.controls.distanceMode.value === 'areas') this.form.controls.placeLabel.setValue(''); if (this.form.controls.distanceMode.value === 'areas') this.form.controls.distanceKm.setValue(0); else if (!this.form.controls.distanceKm.value) this.form.controls.distanceKm.setValue(1); } @@ -190,11 +198,18 @@ export class InvasionAddDialogComponent implements OnInit { this.saving.set(true); const v = this.form.getRawValue(); - const dist = v.distanceMode === 'areas' ? 0 : Math.round((v.distanceKm ?? 1) * 1000); + // One conversion for all three answers, shared with the card chip and the scope sheet. + const scope = scopeToFields( + v.distanceMode === 'areas' + ? { mode: 'profile' } + : { distanceKm: v.distanceKm ?? 1, mode: v.placeLabel ? 'place' : 'profile', placeLabel: v.placeLabel ?? '' }, + ); const creates = targets.map(g => this.invasionService.create({ + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, clean: v.clean ? 1 : 0, - distance: dist, + distance: scope.distance, // Split variants (Mixed Male/Female) carry an implicit gender; typed grunts // fall back to the user's dropdown choice; fixed-gender rows force 0. gender: g.gender ?? (isGenderFixed(g.gruntType) ? 0 : (v.gender ?? 0)), diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/lures/lure-add-dialog.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/lures/lure-add-dialog.component.html index 633424fa..f3e3ed33 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/lures/lure-add-dialog.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/lures/lure-add-dialog.component.html @@ -51,12 +51,23 @@

{{ 'ALARM.LOCATION_MODE' | translate }}

> @if (form.controls.distanceMode.value === 'distance') { - {{ 'ALARM.DISTANCE_LABEL' | translate }}{{ - 'ALARM.DISTANCE_SUFFIX' | translate - }} +
+ + {{ 'WHERE.MEASURED_FROM' | translate }} + + {{ 'WHERE.MY_PIN' | translate }} + @for (place of places.named(); track place.label) { + {{ place.label }} + } + + + {{ 'ALARM.DISTANCE_LABEL' | translate }}{{ + 'ALARM.DISTANCE_SUFFIX' | translate + }} +
} ([]); @@ -82,6 +91,7 @@ export class LureAddDialogComponent { } onDistanceModeChange(): void { + if (this.form.controls.distanceMode.value === 'areas') this.form.controls.placeLabel.setValue(''); if (this.form.controls.distanceMode.value === 'areas') this.form.controls.distanceKm.setValue(0); else if (!this.form.controls.distanceKm.value) this.form.controls.distanceKm.setValue(1); } @@ -90,12 +100,19 @@ export class LureAddDialogComponent { if (this.selectedLureIds().length === 0) return; this.saving.set(true); const v = this.form.getRawValue(); - const dist = v.distanceMode === 'areas' ? 0 : Math.round((v.distanceKm ?? 1) * 1000); + // One conversion for all three answers, shared with the card chip and the scope sheet. + const scope = scopeToFields( + v.distanceMode === 'areas' + ? { mode: 'profile' } + : { distanceKm: v.distanceKm ?? 1, mode: v.placeLabel ? 'place' : 'profile', placeLabel: v.placeLabel ?? '' }, + ); const creates = this.selectedLureIds().map(lureId => this.lureService.create({ + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, // New lures have no prior bits, so compose bits 1 (auto-delete) and 2 (edit-in-place) directly. clean: compose(!!v.clean, !!v.editInPlace, false), - distance: dist, + distance: scope.distance, lureId, template: v.template || null, }), diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/max-battles/max-battle-add-dialog.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/max-battles/max-battle-add-dialog.component.html index 3932aec1..22e2ca96 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/max-battles/max-battle-add-dialog.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/max-battles/max-battle-add-dialog.component.html @@ -85,11 +85,22 @@

{{ 'ALARM.LOCATION_MODE' | translate }}

@if (commonForm.controls.distanceMode.value === 'distance') { - - {{ 'ALARM.DISTANCE_LABEL' | translate }} - - {{ 'ALARM.DISTANCE_SUFFIX' | translate }} - +
+ + {{ 'WHERE.MEASURED_FROM' | translate }} + + {{ 'WHERE.MY_PIN' | translate }} + @for (place of places.named(); track place.label) { + {{ place.label }} + } + + + + {{ 'ALARM.DISTANCE_LABEL' | translate }} + + {{ 'ALARM.DISTANCE_SUFFIX' | translate }} + +
} (null); + + readonly places = inject(PlacesService); saving = signal(false); selectedLevels = signal([]); selectedPokemonIds = signal([]); @@ -111,6 +116,7 @@ export class MaxBattleAddDialogComponent { } onDistanceModeChange(): void { + if (this.commonForm.controls.distanceMode.value === 'areas') this.commonForm.controls.placeLabel.setValue(''); if (this.commonForm.controls.distanceMode.value === 'areas') { this.commonForm.controls.distanceKm.setValue(0); } else { @@ -128,7 +134,12 @@ export class MaxBattleAddDialogComponent { if (!this.canSave()) return; this.saving.set(true); const common = this.commonForm.getRawValue(); - const distanceMeters = common.distanceMode === 'areas' ? 0 : Math.round((common.distanceKm ?? 1) * 1000); + // One conversion for all three answers, shared with the card chip and the scope sheet. + const scope = scopeToFields( + common.distanceMode === 'areas' + ? { mode: 'profile' } + : { distanceKm: common.distanceKm ?? 1, mode: common.placeLabel ? 'place' : 'profile', placeLabel: common.placeLabel ?? '' }, + ); const creates: ReturnType[] = []; @@ -137,8 +148,10 @@ export class MaxBattleAddDialogComponent { for (const levelVal of this.selectedLevels()) { const levelDef = this.levels.find(l => l.value === levelVal); const maxBattle: MaxBattleCreate = { + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, clean: compose(!!common.clean, false, false), - distance: distanceMeters, + distance: scope.distance, evolution: 9000, form: common.form ?? 0, gmax: levelDef?.gmax ? 1 : 0, @@ -154,8 +167,10 @@ export class MaxBattleAddDialogComponent { // By Pokemon — one alarm per selected Pokemon, level = 9000 (any) for (const pokemonId of this.selectedPokemonIds()) { const maxBattle: MaxBattleCreate = { + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, clean: compose(!!common.clean, false, false), - distance: distanceMeters, + distance: scope.distance, evolution: 9000, form: common.form ?? 0, gmax: common.gmaxOnly ? 1 : 0, diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/nests/nest-add-dialog.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/nests/nest-add-dialog.component.html index 70afc509..9deec53e 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/nests/nest-add-dialog.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/nests/nest-add-dialog.component.html @@ -48,12 +48,23 @@

{{ 'ALARM.LOCATION_MODE' | translate }}

> @if (form.controls.distanceMode.value === 'distance') { - {{ 'ALARM.DISTANCE_LABEL' | translate }}{{ - 'ALARM.DISTANCE_SUFFIX' | translate - }} +
+ + {{ 'WHERE.MEASURED_FROM' | translate }} + + {{ 'WHERE.MY_PIN' | translate }} + @for (place of places.named(); track place.label) { + {{ place.label }} + } + + + {{ 'ALARM.DISTANCE_LABEL' | translate }}{{ + 'ALARM.DISTANCE_SUFFIX' | translate + }} +
} ([]); onDistanceModeChange(): void { + if (this.form.controls.distanceMode.value === 'areas') this.form.controls.placeLabel.setValue(''); if (this.form.controls.distanceMode.value === 'areas') this.form.controls.distanceKm.setValue(0); else if (!this.form.controls.distanceKm.value) this.form.controls.distanceKm.setValue(1); } @@ -74,11 +83,18 @@ export class NestAddDialogComponent { if (this.selectedPokemonIds().length === 0) return; this.saving.set(true); const v = this.form.getRawValue(); - const dist = v.distanceMode === 'areas' ? 0 : Math.round((v.distanceKm ?? 1) * 1000); + // One conversion for all three answers, shared with the card chip and the scope sheet. + const scope = scopeToFields( + v.distanceMode === 'areas' + ? { mode: 'profile' } + : { distanceKm: v.distanceKm ?? 1, mode: v.placeLabel ? 'place' : 'profile', placeLabel: v.placeLabel ?? '' }, + ); const creates = this.selectedPokemonIds().map(pokemonId => this.nestService.create({ + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, clean: v.clean ? 1 : 0, - distance: dist, + distance: scope.distance, minSpawnAvg: v.minSpawnAvg ?? 0, pokemonId, template: v.template || null, diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.html index 9ea90a90..de113113 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.html @@ -233,6 +233,19 @@

{{ 'POKEMON.FILTER_SIZE' | translate }}

} } + +
+ {{ 'POKEMON.PVP_EVOLUTION' | translate }} + + {{ 'POKEMON.PVP_EVO_BASE' | translate }} + {{ 'POKEMON.PVP_EVO_MEGA' | translate }} + {{ 'POKEMON.PVP_EVO_MEGA_X' | translate }} + {{ 'POKEMON.PVP_EVO_MEGA_Y' | translate }} + +

+ {{ 'POKEMON.PVP_EVOLUTION_HINT' | translate }} +

+
{{ 'POKEMON.PVP_BEST_RANK' | translate }} diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.spec.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.spec.ts index 9654ebc3..d0910115 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.spec.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.spec.ts @@ -53,7 +53,10 @@ describe('PokemonAddDialogComponent', () => { { provide: MonsterService, useValue: monsterService }, { provide: MasterDataService, useValue: masterData }, { provide: I18nService, useValue: { instant: (k: string) => k } }, - { provide: AlertDefaultsService, useValue: { defaultDistanceKm: () => 1, defaultMode: () => 'areas' } }, + { + provide: AlertDefaultsService, + useValue: { defaultDistanceKm: () => 1, defaultMode: () => 'areas', defaultPlaceLabel: () => '' }, + }, { provide: PoracleConfigService, useValue: { load: () => of({ defaultPvpCap: 0 }), serverConfig: () => ({ pvpCaps: [] }) }, @@ -147,6 +150,27 @@ describe('PokemonAddDialogComponent', () => { ); }); + it('sends the mega mode with a PVP rule', () => { + component.selectedPokemonIds.set([MEOWTH]); + component.pvpForm.controls.pvpRankingLeague.setValue(1500); + component.pvpForm.controls.pvpRankingEvolution.setValue(2); + + component.save(); + + expect((monsterService.create.mock.calls[0][0] as MonsterCreate).pvpRankingEvolution).toBe(2); + }); + + it('does not send a mega mode on a rule with no league', () => { + // Mega mode only means something inside a PVP rule. Carrying it on a non-PVP alarm would be a + // filter the user never asked for, on a field PoracleNG still reads. + component.selectedPokemonIds.set([MEOWTH]); + component.pvpForm.controls.pvpRankingEvolution.setValue(2); + + component.save(); + + expect((monsterService.create.mock.calls[0][0] as MonsterCreate).pvpRankingEvolution).toBe(0); + }); + it('sends the scope fields for an alarm aimed at a saved place', () => { component.selectedPokemonIds.set([MEOWTH]); component.notifForm.controls.distanceMode.setValue('distance'); diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.ts index 61584ade..61fb7d39 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-add-dialog.component.ts @@ -116,6 +116,8 @@ export class PokemonAddDialogComponent implements OnInit { pvpForm = this.fb.group({ pvpRankingBest: [1], pvpRankingCap: [0], + // 0 base, 1 any mega, 2 Mega X, 3 Mega Y — PoracleNG's pvp_ranking_evolution. + pvpRankingEvolution: [0], pvpRankingLeague: [0], pvpRankingMinCp: [0], pvpRankingWorst: [100], @@ -216,6 +218,7 @@ export class PokemonAddDialogComponent implements OnInit { pokemonId, pvpRankingBest: pvp.pvpRankingLeague ? (pvp.pvpRankingBest ?? 1) : 0, pvpRankingCap: pvp.pvpRankingLeague ? (pvp.pvpRankingCap ?? 0) : 0, + pvpRankingEvolution: pvp.pvpRankingLeague ? (pvp.pvpRankingEvolution ?? 0) : 0, pvpRankingLeague: pvp.pvpRankingLeague ?? 0, pvpRankingMinCp: pvp.pvpRankingLeague ? (pvp.pvpRankingMinCp ?? 0) : 0, pvpRankingWorst: pvp.pvpRankingLeague ? (pvp.pvpRankingWorst ?? 100) : 4096, diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-edit-dialog.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-edit-dialog.component.html index 65ebbc3c..028b2ade 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-edit-dialog.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-edit-dialog.component.html @@ -202,6 +202,16 @@

{{ 'POKEMON.FILTER_SIZE' | translate }}

{{ 'POKEMON.PVP_CAP_LEVEL' | translate: { level: cap } }} } + +
+ {{ 'POKEMON.PVP_EVOLUTION' | translate }} + + {{ 'POKEMON.PVP_EVO_BASE' | translate }} + {{ 'POKEMON.PVP_EVO_MEGA' | translate }} + {{ 'POKEMON.PVP_EVO_MEGA_X' | translate }} + {{ 'POKEMON.PVP_EVO_MEGA_Y' | translate }} + +
}
diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-edit-dialog.component.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-edit-dialog.component.ts index 19863a6d..7dc90575 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-edit-dialog.component.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-edit-dialog.component.ts @@ -92,6 +92,8 @@ export class PokemonEditDialogComponent implements OnInit { placeLabel: [this.data.overrideLocationLabel ?? ''], pvpRankingBest: [this.data.pvpRankingBest], pvpRankingCap: [this.data.pvpRankingCap ?? 0], + // 0 base, 1 any mega, 2 Mega X, 3 Mega Y — PoracleNG's pvp_ranking_evolution. + pvpRankingEvolution: [this.data.pvpRankingEvolution ?? 0], pvpRankingLeague: [this.data.pvpRankingLeague], pvpRankingMinCp: [this.data.pvpRankingMinCp], pvpRankingWorst: [this.data.pvpRankingWorst], @@ -178,6 +180,7 @@ export class PokemonEditDialogComponent implements OnInit { minWeight: values.minWeight ?? 0, pvpRankingBest: values.pvpRankingLeague ? (values.pvpRankingBest ?? 1) : 0, pvpRankingCap: values.pvpRankingLeague ? (values.pvpRankingCap ?? 0) : 0, + pvpRankingEvolution: values.pvpRankingLeague ? (values.pvpRankingEvolution ?? 0) : 0, pvpRankingLeague: values.pvpRankingLeague ?? 0, pvpRankingMinCp: values.pvpRankingLeague ? (values.pvpRankingMinCp ?? 0) : 0, pvpRankingWorst: values.pvpRankingLeague ? (values.pvpRankingWorst ?? 100) : 4096, diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-list.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-list.component.html index 3b1343fa..acaddf38 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-list.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-list.component.html @@ -213,6 +213,9 @@

{{ getPokemonName(monster.pokemonId) }}

emoji_events {{ getLeagueName(monster.pvpRankingLeague) }} {{ 'POKEMON.PVP_LEAGUE_SUFFIX' | translate }} + @if (monster.pvpRankingEvolution) { + · {{ megaLabel(monster.pvpRankingEvolution) | translate }} + } {{ 'POKEMON.PVP_RANK_RANGE' | translate: { best: monster.pvpRankingBest, worst: monster.pvpRankingWorst } diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-list.component.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-list.component.ts index fc390b0f..5c9694ec 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-list.component.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/pokemon/pokemon-list.component.ts @@ -409,6 +409,20 @@ export class PokemonListComponent implements OnInit { }); } + /** The i18n key for a rule's mega mode, or an empty string for a base-forms rule. */ + megaLabel(evolution: number): string { + switch (evolution) { + case 1: + return 'POKEMON.PVP_EVO_MEGA'; + case 2: + return 'POKEMON.PVP_EVO_MEGA_X'; + case 3: + return 'POKEMON.PVP_EVO_MEGA_Y'; + default: + return ''; + } + } + ngOnInit(): void { // Ensure masterdata is loaded this.masterData.loadData().pipe(takeUntilDestroyed(this.destroyRef)).subscribe(); diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/quests/quest-add-dialog.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/quests/quest-add-dialog.component.html index 53f93cc7..66052323 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/quests/quest-add-dialog.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/quests/quest-add-dialog.component.html @@ -83,11 +83,22 @@

{{ 'ALARM.LOCATION_MODE' | translate }}

@if (commonForm.controls.distanceMode.value === 'distance') { - - {{ 'ALARM.DISTANCE_LABEL' | translate }} - - {{ 'ALARM.DISTANCE_SUFFIX' | translate }} - +
+ + {{ 'WHERE.MEASURED_FROM' | translate }} + + {{ 'WHERE.MY_PIN' | translate }} + @for (place of places.named(); track place.label) { + {{ place.label }} + } + + + + {{ 'ALARM.DISTANCE_LABEL' | translate }} + + {{ 'ALARM.DISTANCE_SUFFIX' | translate }} + +
} ); readonly iconService = inject(IconService); - readonly isWebhook = inject(AuthService).isImpersonating(); + readonly isWebhook = inject(AuthService).isImpersonating(); itemForm = this.fb.group({ reward: [0], }); + readonly places = inject(PlacesService); + /** Quest-relevant items (balls, berries, potions, revives, TMs, etc.) */ readonly questItems = signal<{ id: number; name: string }[]>([]); @@ -121,6 +127,7 @@ export class QuestAddDialogComponent { } onDistanceModeChange(): void { + if (this.commonForm.controls.distanceMode.value === 'areas') this.commonForm.controls.placeLabel.setValue(''); if (this.commonForm.controls.distanceMode.value === 'areas') { this.commonForm.controls.distanceKm.setValue(0); } else { @@ -147,7 +154,12 @@ export class QuestAddDialogComponent { if (!this.canSave()) return; this.saving.set(true); const common = this.commonForm.getRawValue(); - const distanceMeters = common.distanceMode === 'areas' ? 0 : Math.round((common.distanceKm ?? 1) * 1000); + // One conversion for all three answers, shared with the card chip and the scope sheet. + const scope = scopeToFields( + common.distanceMode === 'areas' + ? { mode: 'profile' } + : { distanceKm: common.distanceKm ?? 1, mode: common.placeLabel ? 'place' : 'profile', placeLabel: common.placeLabel ?? '' }, + ); // New alarms have no prior bits, so compose directly from the two surfaced toggles (edit-in-place unsupported for quests). const cleanValue = compose(!!common.clean, false, !!common.summary); @@ -158,8 +170,10 @@ export class QuestAddDialogComponent { for (const pokemonId of this.selectedPokemonIds()) { creates.push( this.questService.create({ + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, clean: cleanValue, - distance: distanceMeters, + distance: scope.distance, pokemonId, reward: pokemonId, rewardType: 7, @@ -172,8 +186,10 @@ export class QuestAddDialogComponent { case 1: creates.push( this.questService.create({ + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, clean: cleanValue, - distance: distanceMeters, + distance: scope.distance, pokemonId: 0, reward: this.itemForm.controls.reward.value ?? 0, rewardType: 2, @@ -186,8 +202,10 @@ export class QuestAddDialogComponent { for (const pokemonId of this.selectedMegaPokemonIds()) { creates.push( this.questService.create({ + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, clean: cleanValue, - distance: distanceMeters, + distance: scope.distance, pokemonId, reward: pokemonId, rewardType: 12, @@ -201,8 +219,10 @@ export class QuestAddDialogComponent { for (const pokemonId of this.selectedCandyPokemonIds()) { creates.push( this.questService.create({ + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, clean: cleanValue, - distance: distanceMeters, + distance: scope.distance, pokemonId, reward: pokemonId, rewardType: 4, diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/raids/raid-add-dialog.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/raids/raid-add-dialog.component.html index aa2816fe..18fd48fa 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/raids/raid-add-dialog.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/raids/raid-add-dialog.component.html @@ -91,11 +91,22 @@

{{ 'ALARM.LOCATION_MODE' | translate }}

@if (commonForm.controls.distanceMode.value === 'distance') { - - {{ 'ALARM.DISTANCE_LABEL' | translate }} - - {{ 'ALARM.DISTANCE_SUFFIX' | translate }} - +
+ + {{ 'WHERE.MEASURED_FROM' | translate }} + + {{ 'WHERE.MY_PIN' | translate }} + @for (place of places.named(); track place.label) { + {{ place.label }} + } + + + + {{ 'ALARM.DISTANCE_LABEL' | translate }} + + {{ 'ALARM.DISTANCE_SUFFIX' | translate }} + +
} ); + readonly isWebhook = inject(AuthService).isImpersonating(); + readonly places = inject(PlacesService); saving = signal(false); selectedEggLevels = signal([]); selectedGymId = signal(null); @@ -94,6 +99,7 @@ export class RaidAddDialogComponent { /** Boss tab is single-select; the selector emits an array of length 0 or 1. */ onDistanceModeChange(): void { + if (this.commonForm.controls.distanceMode.value === 'areas') this.commonForm.controls.placeLabel.setValue(''); if (this.commonForm.controls.distanceMode.value === 'areas') { this.commonForm.controls.distanceKm.setValue(0); } else { @@ -111,7 +117,12 @@ export class RaidAddDialogComponent { if (!this.canSave()) return; this.saving.set(true); const common = this.commonForm.getRawValue(); - const distanceMeters = common.distanceMode === 'areas' ? 0 : Math.round((common.distanceKm ?? 1) * 1000); + // One conversion for all three answers, shared with the card chip and the scope sheet. + const scope = scopeToFields( + common.distanceMode === 'areas' + ? { mode: 'profile' } + : { distanceKm: common.distanceKm ?? 1, mode: common.placeLabel ? 'place' : 'profile', placeLabel: common.placeLabel ?? '' }, + ); // clean is a PoracleNG bitmask: bit 1 = auto-delete, bit 2 = edit-in-place, bit 4 = summary. // RSVP modes (1/2) need the edit bit so count changes edit the alert instead of re-sending. // New alarms have no prior bits, so there is nothing to preserve here. @@ -125,8 +136,10 @@ export class RaidAddDialogComponent { // By Level for (const level of this.selectedRaidLevels()) { const raid: RaidCreate = { + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, clean, - distance: distanceMeters, + distance: scope.distance, evolution: 9000, exclusive: 0, form: 0, @@ -142,8 +155,10 @@ export class RaidAddDialogComponent { } for (const level of this.selectedEggLevels()) { const egg: EggCreate = { + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, clean, - distance: distanceMeters, + distance: scope.distance, exclusive: 0, gymId: this.selectedGymId() ?? '', level, @@ -159,8 +174,10 @@ export class RaidAddDialogComponent { // picker whose value could not survive the request. See #615. for (const pokemonId of this.selectedPokemonIds()) { const raid: RaidCreate = { + overrideAreas: scope.overrideAreas, + overrideLocationLabel: scope.overrideLocationLabel, clean, - distance: distanceMeters, + distance: scope.distance, evolution: 9000, exclusive: 0, form: 0, diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/alert-defaults-dialog/alert-defaults-dialog.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/alert-defaults-dialog/alert-defaults-dialog.component.html index 7616aba1..d5d9c196 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/alert-defaults-dialog/alert-defaults-dialog.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/alert-defaults-dialog/alert-defaults-dialog.component.html @@ -37,6 +37,16 @@

{{ 'ALERT_DEFAULTS.DEFAULT_DISTANCE_HINT' | translate }} } + + + {{ 'WHERE.MEASURED_FROM' | translate }} + + {{ 'WHERE.MY_PIN' | translate }} + @for (place of places.named(); track place.label) { + {{ place.label }} + } + + } diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/alert-defaults-dialog/alert-defaults-dialog.component.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/alert-defaults-dialog/alert-defaults-dialog.component.ts index 2689a373..6bb1df77 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/alert-defaults-dialog/alert-defaults-dialog.component.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/alert-defaults-dialog/alert-defaults-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, signal } from '@angular/core'; +import { Component, OnInit, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; @@ -6,14 +6,17 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; +import { MatSelectModule } from '@angular/material/select'; import { TranslatePipe } from '@ngx-translate/core'; +import { SavedPlaces } from '../../../core/models'; import { AlertDefaultsService, AlertLocationMode, MAX_DEFAULT_DISTANCE_KM, MIN_DEFAULT_DISTANCE_KM, } from '../../../core/services/alert-defaults.service'; +import { PlacesService } from '../../../core/services/places.service'; import { DeliveryPreviewComponent } from '../delivery-preview/delivery-preview.component'; @Component({ @@ -24,6 +27,7 @@ import { DeliveryPreviewComponent } from '../delivery-preview/delivery-preview.c MatFormFieldModule, MatInputModule, MatRadioModule, + MatSelectModule, MatIconModule, DeliveryPreviewComponent, TranslatePipe, @@ -33,17 +37,21 @@ import { DeliveryPreviewComponent } from '../delivery-preview/delivery-preview.c styleUrl: './alert-defaults-dialog.component.scss', templateUrl: './alert-defaults-dialog.component.html', }) -export class AlertDefaultsDialogComponent { +export class AlertDefaultsDialogComponent implements OnInit { private readonly alertDefaults = inject(AlertDefaultsService); - readonly dialogRef = inject(MatDialogRef); distanceKm = this.alertDefaults.defaultDistanceKm(); + readonly maxKm = MAX_DEFAULT_DISTANCE_KM; readonly minKm = MIN_DEFAULT_DISTANCE_KM; mode = signal(this.alertDefaults.defaultMode()); + /** Empty means the profile pin, which is what new alarms did before per-alarm scope. */ + placeLabel = this.alertDefaults.defaultPlaceLabel(); + readonly places = inject(PlacesService); + get canSave(): boolean { return this.distanceError === null; } @@ -60,9 +68,22 @@ export class AlertDefaultsDialogComponent { return null; } + ngOnInit(): void { + this.loadPlaces(); + } + save(): void { if (!this.canSave) return; - this.alertDefaults.save(this.mode(), this.distanceKm); + this.alertDefaults.save(this.mode(), this.distanceKm, this.placeLabel); this.dialogRef.close(true); } + + private loadPlaces(): void { + this.places.load().subscribe({ + error: () => undefined, + // A place the user has since deleted would otherwise keep seeding new alarms with a label + // PoracleNG rejects, and the select would show a blank row for it. + next: (places: SavedPlaces) => this.alertDefaults.reconcilePlace(places.named.map(p => p.label)), + }); + } } diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/da.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/da.json index c61d6b16..02cb9583 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/da.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/da.json @@ -182,6 +182,12 @@ "ARIA_LABEL": "Velkomst-onboarding" }, "POKEMON": { + "PVP_EVOLUTION": "Mega-udvikling", + "PVP_EVOLUTION_HINT": "Ranger grundformerne eller en mega. Megaer rangeres separat, så en mega-regel matcher ikke en grundform.", + "PVP_EVO_BASE": "Grund", + "PVP_EVO_MEGA": "Mega", + "PVP_EVO_MEGA_X": "Mega X", + "PVP_EVO_MEGA_Y": "Mega Y", "PAGE_TITLE": "Pokemon-alarmer", "PAGE_DESC": "Spor vilde Pokemon-spawns med brugerdefinerede IV-, CP-, niveau- og PVP-filtre.", "SEARCH_PLACEHOLDER": "Søg efter navn eller #...", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/de.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/de.json index 16b6486c..41574272 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/de.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/de.json @@ -182,6 +182,12 @@ "ARIA_LABEL": "Willkommens-Einführung" }, "POKEMON": { + "PVP_EVOLUTION": "Mega-Entwicklung", + "PVP_EVOLUTION_HINT": "Werte die Grundformen oder eine Mega-Form. Megas werden getrennt gewertet, eine Mega-Regel trifft also keine Grundform.", + "PVP_EVO_BASE": "Basis", + "PVP_EVO_MEGA": "Mega", + "PVP_EVO_MEGA_X": "Mega X", + "PVP_EVO_MEGA_Y": "Mega Y", "PAGE_TITLE": "Pokemon-Alarme", "PAGE_DESC": "Verfolge wilde Pokemon-Spawns mit individuellen IV-, CP-, Level- und PVP-Filtern.", "SEARCH_PLACEHOLDER": "Suche nach Name oder #...", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/en.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/en.json index a908a925..8994cd64 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/en.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/en.json @@ -182,6 +182,12 @@ "ARIA_LABEL": "Welcome onboarding" }, "POKEMON": { + "PVP_EVOLUTION": "Mega evolution", + "PVP_EVOLUTION_HINT": "Rank the base forms, or a mega. Megas are ranked separately, so a mega rule will not match a base-form spawn.", + "PVP_EVO_BASE": "Base", + "PVP_EVO_MEGA": "Mega", + "PVP_EVO_MEGA_X": "Mega X", + "PVP_EVO_MEGA_Y": "Mega Y", "PAGE_TITLE": "Pokemon Alarms", "PAGE_DESC": "Track wild Pokemon spawns with custom IV, CP, level, and PVP filters.", "SEARCH_PLACEHOLDER": "Search by name or #...", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/es.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/es.json index e7160d08..5e08c247 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/es.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/es.json @@ -182,6 +182,12 @@ "ARIA_LABEL": "Bienvenida de inicio" }, "POKEMON": { + "PVP_EVOLUTION": "Megaevolución", + "PVP_EVOLUTION_HINT": "Clasifica las formas base o una mega. Las megas se clasifican aparte, así que una regla mega no coincidirá con una forma base.", + "PVP_EVO_BASE": "Base", + "PVP_EVO_MEGA": "Mega", + "PVP_EVO_MEGA_X": "Mega X", + "PVP_EVO_MEGA_Y": "Mega Y", "PAGE_TITLE": "Alarmas Pokemon", "PAGE_DESC": "Rastrea spawns salvajes de Pokemon con filtros personalizados de IV, CP, nivel y PVP.", "SEARCH_PLACEHOLDER": "Buscar por nombre o #...", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/fr.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/fr.json index d7921ec5..8944af44 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/fr.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/fr.json @@ -182,6 +182,12 @@ "ARIA_LABEL": "Assistant de bienvenue" }, "POKEMON": { + "PVP_EVOLUTION": "Méga-évolution", + "PVP_EVOLUTION_HINT": "Classez les formes de base ou une méga. Les mégas sont classées à part : une règle méga ne correspondra pas à une forme de base.", + "PVP_EVO_BASE": "Base", + "PVP_EVO_MEGA": "Méga", + "PVP_EVO_MEGA_X": "Méga X", + "PVP_EVO_MEGA_Y": "Méga Y", "PAGE_TITLE": "Alarmes Pokemon", "PAGE_DESC": "Suis les spawns sauvages de Pokemon avec des filtres IV, CP, niveau et PVP personnalisés.", "SEARCH_PLACEHOLDER": "Rechercher par nom ou #...", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/it.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/it.json index 04ab2e10..d79dd5a3 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/it.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/it.json @@ -182,6 +182,12 @@ "ARIA_LABEL": "Benvenuto iniziale" }, "POKEMON": { + "PVP_EVOLUTION": "Megaevoluzione", + "PVP_EVOLUTION_HINT": "Classifica le forme base o una mega. Le mega hanno classifiche separate, quindi una regola mega non corrisponderà a una forma base.", + "PVP_EVO_BASE": "Base", + "PVP_EVO_MEGA": "Mega", + "PVP_EVO_MEGA_X": "Mega X", + "PVP_EVO_MEGA_Y": "Mega Y", "PAGE_TITLE": "Allarmi Pokemon", "PAGE_DESC": "Monitora gli spawn di Pokemon selvatici con filtri personalizzati per IV, CP, livello e PVP.", "SEARCH_PLACEHOLDER": "Cerca per nome o #...", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/nl.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/nl.json index a83affa0..cc8ed20c 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/nl.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/nl.json @@ -182,6 +182,12 @@ "ARIA_LABEL": "Welkom onboarding" }, "POKEMON": { + "PVP_EVOLUTION": "Mega-evolutie", + "PVP_EVOLUTION_HINT": "Rangschik de basisvormen of een mega. Mega's worden apart gerangschikt, dus een mega-regel matcht geen basisvorm.", + "PVP_EVO_BASE": "Basis", + "PVP_EVO_MEGA": "Mega", + "PVP_EVO_MEGA_X": "Mega X", + "PVP_EVO_MEGA_Y": "Mega Y", "PAGE_TITLE": "Pokemon Alarmen", "PAGE_DESC": "Volg wilde Pokemon spawns met aangepaste IV, CP, level en PVP filters.", "SEARCH_PLACEHOLDER": "Zoek op naam of #...", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pl.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pl.json index da174cf9..19f4f12b 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pl.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pl.json @@ -182,6 +182,12 @@ "ARIA_LABEL": "Powitanie i konfiguracja" }, "POKEMON": { + "PVP_EVOLUTION": "Megaewolucja", + "PVP_EVOLUTION_HINT": "Oceniaj formy podstawowe albo megę. Megi mają osobny ranking, więc reguła megi nie dopasuje formy podstawowej.", + "PVP_EVO_BASE": "Podstawowa", + "PVP_EVO_MEGA": "Mega", + "PVP_EVO_MEGA_X": "Mega X", + "PVP_EVO_MEGA_Y": "Mega Y", "PAGE_TITLE": "Alarmy Pokemon", "PAGE_DESC": "Śledź dzikie spawny Pokemon z niestandardowymi filtrami IV, CP, poziomu i PVP.", "SEARCH_PLACEHOLDER": "Szukaj po nazwie lub #...", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt-BR.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt-BR.json index d8db85aa..bf0627a3 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt-BR.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt-BR.json @@ -182,6 +182,12 @@ "ARIA_LABEL": "Onboarding de boas-vindas" }, "POKEMON": { + "PVP_EVOLUTION": "Megaevolução", + "PVP_EVOLUTION_HINT": "Classifique as formas base ou uma mega. As megas são classificadas à parte, então uma regra mega não corresponde a uma forma base.", + "PVP_EVO_BASE": "Base", + "PVP_EVO_MEGA": "Mega", + "PVP_EVO_MEGA_X": "Mega X", + "PVP_EVO_MEGA_Y": "Mega Y", "PAGE_TITLE": "Alarmes de Pokemon", "PAGE_DESC": "Rastreie spawns selvagens de Pokemon com filtros personalizados de IV, CP, nível e PVP.", "SEARCH_PLACEHOLDER": "Buscar por nome ou #...", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt.json index b2a31a45..39ae39ea 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt.json @@ -182,6 +182,12 @@ "ARIA_LABEL": "Boas-vindas de integração" }, "POKEMON": { + "PVP_EVOLUTION": "Megaevolução", + "PVP_EVOLUTION_HINT": "Classifica as formas base ou uma mega. As megas são classificadas à parte, por isso uma regra mega não corresponde a uma forma base.", + "PVP_EVO_BASE": "Base", + "PVP_EVO_MEGA": "Mega", + "PVP_EVO_MEGA_X": "Mega X", + "PVP_EVO_MEGA_Y": "Mega Y", "PAGE_TITLE": "Alarmes de Pokemon", "PAGE_DESC": "Monitoriza spawns de Pokemon selvagens com filtros personalizados de IV, CP, nível e PVP.", "SEARCH_PLACEHOLDER": "Pesquisar por nome ou #...", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/sv.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/sv.json index 81dd38a3..a373cce0 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/sv.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/sv.json @@ -182,6 +182,12 @@ "ARIA_LABEL": "Välkomst-onboarding" }, "POKEMON": { + "PVP_EVOLUTION": "Megautveckling", + "PVP_EVOLUTION_HINT": "Ranka grundformerna eller en mega. Megor rankas separat, så en megaregel matchar inte en grundform.", + "PVP_EVO_BASE": "Grund", + "PVP_EVO_MEGA": "Mega", + "PVP_EVO_MEGA_X": "Mega X", + "PVP_EVO_MEGA_Y": "Mega Y", "PAGE_TITLE": "Pokemon-larm", "PAGE_DESC": "Spåra vilda Pokemon-spawns med anpassade IV-, CP-, nivå- och PVP-filter.", "SEARCH_PLACEHOLDER": "Sök efter namn eller #...", diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ff13cd0..c0e82f81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **New alarms can start somewhere other than your pin.** Alert Defaults gains a place alongside the radius, and all ten add dialogs now offer a "measured from" selector, so a preference like "within 2 km of work" applies to everything you create instead of being re-picked each time. A place that gets deleted is forgotten rather than left seeding alarms with a label PoracleNG rejects ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). +- **PVP rules can target mega evolutions.** PoracleNG 5.1.0 ranks megas separately from base forms, so a Pokemon PVP rule now chooses between Base, Mega, Mega X and Mega Y, and the card's PVP badge says which. Works regardless of the server's `include_mega_evolution` setting, which only decides the default for rules that do not ask ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). + - **Every alarm type says where it reaches you, in your own language.** The scope chip now appears on raid, egg, quest, invasion, lure, nest, gym, fort-change and max-battle cards as well as Pokemon, and clicking it opens the same picker. Raids, eggs, quests and max battles got it through the shared alarm-info summary, so four types describe themselves correctly from one implementation rather than four. Edit dialogs show the scope read-only and hide the areas-or-distance control for an area-confined alarm, which that control cannot describe and a save would have had refused. All 37 strings are translated into the ten non-English locales ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). - **Alarms can be aimed somewhere other than your pin.** PoracleNG 5.1.0 gives every alarm its own delivery scope, and the API now carries it: an alarm can measure its radius from a saved place ("within 2 km of work") or be confined to a set of areas, instead of inheriting the profile's single pin and area list. Saved places are managed at `GET/POST /api/location/places` and `DELETE /api/location/places/{label}`; deleting a place that alarms still point at answers 409 and names them rather than orphaning the label. The three ways a scope can contradict itself — a place and areas together, areas with a radius, a place without one — are refused before anything is written, with wording that says which one to change ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). From 705b9ccd0cf4c1cb0d2a9970c0ea9271176e3cf0 Mon Sep 17 00:00:00 2001 From: hokiepokedad2 <38219945+hokiepokedad2@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:00:18 -0400 Subject: [PATCH 2/3] fix(where): Places belongs in the nav, not the user menu Places was a dialog next to Alert Defaults. Wrong on two counts. It is data, not a preference. Alert Defaults is localStorage; Places is server-side CRUD, exactly like My Geofences, which has its own nav item. And the Areas page already answers this question: it opens with two method cards, Areas and Location, where the Location card holds the pin and its Set/Change button. Named points are just more pins, so hiding them in a settings menu split one idea across two surfaces. - The dialog becomes a page at /places, in the settings nav group beside Areas and My Geofences. The user-menu entry and its string are gone. - The route carries disabledFeatureGuard('disable_location'), matching LocationController, which already gates the endpoints it calls. A nav item alone would have left the page reachable by URL. - The Areas location card links to it, so the two surfaces reference each other rather than one being hidden. Also caught while doing it: NAV.PLACES was missing from all eleven locale files, English included, so the sidebar would have rendered the literal key. My earlier parity check could not see it, because it compares locales against English and English was missing it too. Added a check that reads the keys templates actually ask for and asserts each exists in en.json. It found two more, COMMON.ERROR and COMMON.SAVED, both pre-existing in the invasion, max-battle and quest add dialogs and left alone here. --- .../ClientApp/src/app/app.html | 4 --- .../ClientApp/src/app/app.routes.ts | 5 +++ .../ClientApp/src/app/app.ts | 13 +++++--- .../modules/areas/area-list.component.html | 4 +++ .../app/modules/areas/area-list.component.ts | 2 ++ .../places/places.component.html} | 31 ++++++++++--------- .../places/places.component.scss} | 0 .../places/places.component.spec.ts} | 19 ++++++------ .../places/places.component.ts} | 25 +++++++++------ .../ClientApp/src/assets/i18n/da.json | 6 ++-- .../ClientApp/src/assets/i18n/de.json | 6 ++-- .../ClientApp/src/assets/i18n/en.json | 6 ++-- .../ClientApp/src/assets/i18n/es.json | 6 ++-- .../ClientApp/src/assets/i18n/fr.json | 6 ++-- .../ClientApp/src/assets/i18n/it.json | 6 ++-- .../ClientApp/src/assets/i18n/nl.json | 6 ++-- .../ClientApp/src/assets/i18n/pl.json | 6 ++-- .../ClientApp/src/assets/i18n/pt-BR.json | 6 ++-- .../ClientApp/src/assets/i18n/pt.json | 6 ++-- .../ClientApp/src/assets/i18n/sv.json | 6 ++-- CHANGELOG.md | 2 ++ 21 files changed, 105 insertions(+), 66 deletions(-) rename Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/{shared/components/places-dialog/places-dialog.component.html => modules/places/places.component.html} (80%) rename Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/{shared/components/places-dialog/places-dialog.component.scss => modules/places/places.component.scss} (100%) rename Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/{shared/components/places-dialog/places-dialog.component.spec.ts => modules/places/places.component.spec.ts} (88%) rename Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/{shared/components/places-dialog/places-dialog.component.ts => modules/places/places.component.ts} (83%) diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/app.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/app.html index 61e0a77f..70e3398f 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/app.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/app.html @@ -121,10 +121,6 @@ tune {{ 'MENU.ALERT_DEFAULTS' | translate }} - + + place + {{ 'AREAS.MANAGE_PLACES' | translate }} +

diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/areas/area-list.component.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/areas/area-list.component.ts index b1a5aded..66722a5f 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/areas/area-list.component.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/areas/area-list.component.ts @@ -11,6 +11,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { RouterLink } from '@angular/router'; import { TranslatePipe } from '@ngx-translate/core'; import { AreaDefinition, GeofenceData, Location } from '../../core/models'; @@ -52,6 +53,7 @@ interface GroupInfo { AreaMapComponent, LanguageSelectorComponent, RegionSelectorComponent, + RouterLink, ], selector: 'app-area-list', standalone: true, diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/places-dialog/places-dialog.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.html similarity index 80% rename from Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/places-dialog/places-dialog.component.html rename to Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.html index 2f9bcc73..64fa6319 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/places-dialog/places-dialog.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.html @@ -1,9 +1,18 @@ -

{{ 'WHERE.PLACES_TITLE' | translate }}

+ - - @if (loading()) { -
- } @else { +@if (loading()) { +
+} @else { +
@if (places.pin(); as pin) {
my_location @@ -36,13 +45,5 @@

{{ 'WHERE.PLACES_TITLE' | translate }}

} } - } - - - - - - +
+} diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/places-dialog/places-dialog.component.scss b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.scss similarity index 100% rename from Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/places-dialog/places-dialog.component.scss rename to Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.scss diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/places-dialog/places-dialog.component.spec.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.spec.ts similarity index 88% rename from Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/places-dialog/places-dialog.component.spec.ts rename to Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.spec.ts index 5182f1ec..212a1a2f 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/places-dialog/places-dialog.component.spec.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.spec.ts @@ -1,16 +1,16 @@ import { HttpErrorResponse, provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; -import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { provideTranslateService } from '@ngx-translate/core'; import { of, throwError } from 'rxjs'; -import { PlacesDialogComponent } from './places-dialog.component'; -import { ConfigService } from '../../../core/services/config.service'; -import { PlacesService } from '../../../core/services/places.service'; +import { PlacesComponent } from './places.component'; +import { ConfigService } from '../../core/services/config.service'; +import { PlacesService } from '../../core/services/places.service'; -describe('PlacesDialogComponent', () => { +describe('PlacesComponent', () => { let dialog: { open: jest.Mock }; let places: { add: jest.Mock; @@ -26,7 +26,7 @@ describe('PlacesDialogComponent', () => { results.forEach(result => dialog.open.mockReturnValueOnce({ afterClosed: () => of(result) })); } - function create(): PlacesDialogComponent { + function create(): PlacesComponent { dialog = { open: jest.fn() }; snackBar = { open: jest.fn() }; places = { @@ -41,7 +41,6 @@ describe('PlacesDialogComponent', () => { TestBed.configureTestingModule({ providers: [ provideTranslateService(), - { provide: MatDialogRef, useValue: { close: jest.fn() } }, { provide: MatDialog, useValue: dialog }, { provide: MatSnackBar, useValue: snackBar }, { provide: PlacesService, useValue: places }, @@ -49,12 +48,12 @@ describe('PlacesDialogComponent', () => { provideHttpClient(), provideHttpClientTesting(), ], - imports: [PlacesDialogComponent], + imports: [PlacesComponent], }); // MatDialogModule is in the component's own imports, so its MatDialog wins over the TestBed // provider. Overriding at the component injector is the only level that beats it. - TestBed.overrideComponent(PlacesDialogComponent, { + TestBed.overrideComponent(PlacesComponent, { set: { providers: [ { provide: MatDialog, useValue: dialog }, @@ -63,7 +62,7 @@ describe('PlacesDialogComponent', () => { }, }); - const component = TestBed.createComponent(PlacesDialogComponent).componentInstance; + const component = TestBed.createComponent(PlacesComponent).componentInstance; component.ngOnInit(); return component; } diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/places-dialog/places-dialog.component.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.ts similarity index 83% rename from Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/places-dialog/places-dialog.component.ts rename to Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.ts index 135009ea..a2f74706 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/places-dialog/places-dialog.component.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.ts @@ -2,34 +2,39 @@ import { DecimalPipe } from '@angular/common'; import { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectionStrategy, Component, OnInit, inject, signal } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; -import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { MatDialog } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBar } from '@angular/material/snack-bar'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { Location, SavedPlace } from '../../../core/models'; -import { PlacesService } from '../../../core/services/places.service'; -import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'; -import { LocationDialogComponent } from '../location-dialog/location-dialog.component'; +import { Location, SavedPlace } from '../../core/models'; +import { PlacesService } from '../../core/services/places.service'; +import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component'; +import { LocationDialogComponent } from '../../shared/components/location-dialog/location-dialog.component'; /** * The places a user's alarms can be aimed at: the profile pin, plus whatever they have named. * + * A page rather than a dialog, and in the nav beside Areas and My Geofences, because these are the + * user's data and not a preference. The Areas page already frames alerts as reaching you by area or by + * a radius from a point; named points are more of the second, so hiding them in a settings menu split + * one idea across two surfaces. + * * Adding a place borrows the location dialog as a coordinate picker rather than growing a second map, * then asks for the name separately, because picking a point and naming it are two decisions and * putting them on one screen makes both feel like a form. */ @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [DecimalPipe, MatButtonModule, MatDialogModule, MatIconModule, MatListModule, MatProgressSpinnerModule, TranslatePipe], - selector: 'app-places-dialog', + imports: [DecimalPipe, MatButtonModule, MatIconModule, MatListModule, MatProgressSpinnerModule, TranslatePipe], + selector: 'app-places', standalone: true, - styleUrl: './places-dialog.component.scss', - templateUrl: './places-dialog.component.html', + styleUrl: './places.component.scss', + templateUrl: './places.component.html', }) -export class PlacesDialogComponent implements OnInit { +export class PlacesComponent implements OnInit { private readonly dialog = inject(MatDialog); private readonly snackBar = inject(MatSnackBar); private readonly translate = inject(TranslateService); diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/da.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/da.json index 02cb9583..654abbe4 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/da.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/da.json @@ -1,5 +1,6 @@ { "NAV": { + "PLACES": "Steder", "GROUP_ALARMS": "Alarmer", "GROUP_SETTINGS": "Indstillinger", "GROUP_ADMIN": "Admin", @@ -63,8 +64,7 @@ "ACCENT_MYSTIC": "Mystic", "ACCENT_VALOR": "Valor", "ACCENT_INSTINCT": "Instinct", - "ALERT_DEFAULTS": "Standardindstillinger for advarsler", - "PLACES": "Steder" + "ALERT_DEFAULTS": "Standardindstillinger for advarsler" }, "SHORTCUTS": { "TITLE": "Tastaturgenveje", @@ -775,6 +775,7 @@ "SNACK_UPDATED": "Max Battle-alarm opdateret" }, "AREAS": { + "MANAGE_PLACES": "Administrer steder", "PAGE_TITLE": "Områder og placering", "PAGE_DESC": "Kontrollér, hvor du modtager notifikationer.", "METHOD_AREAS": "Områder", @@ -1722,6 +1723,7 @@ "LAST_PAGE": "Sidste side" }, "WHERE": { + "PLACES_PAGE_DESC": "Navngivne punkter, dine beskeder kan rettes mod i stedet for din position.", "ADD_PLACE": "Tilføj et sted", "AREAS_LABEL": "Områder", "AREA_LIST_MORE": "{{areas}} og {{count}} mere", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/de.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/de.json index 41574272..a9facc61 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/de.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/de.json @@ -1,5 +1,6 @@ { "NAV": { + "PLACES": "Orte", "GROUP_ALARMS": "Alarme", "GROUP_SETTINGS": "Einstellungen", "GROUP_ADMIN": "Admin", @@ -63,8 +64,7 @@ "ACCENT_MYSTIC": "Mystic", "ACCENT_VALOR": "Valor", "ACCENT_INSTINCT": "Instinct", - "ALERT_DEFAULTS": "Benachrichtigungs-Standards", - "PLACES": "Orte" + "ALERT_DEFAULTS": "Benachrichtigungs-Standards" }, "SHORTCUTS": { "TITLE": "Tastenkürzel", @@ -775,6 +775,7 @@ "SNACK_UPDATED": "Dynamax-Kampfalarm aktualisiert" }, "AREAS": { + "MANAGE_PLACES": "Orte verwalten", "PAGE_TITLE": "Gebiete & Standort", "PAGE_DESC": "Bestimme, wo du Benachrichtigungen erhältst.", "METHOD_AREAS": "Gebiete", @@ -1722,6 +1723,7 @@ "LAST_PAGE": "Letzte Seite" }, "WHERE": { + "PLACES_PAGE_DESC": "Benannte Punkte, auf die deine Meldungen statt auf deinen Standort ausgerichtet werden können.", "ADD_PLACE": "Ort hinzufügen", "AREAS_LABEL": "Gebiete", "AREA_LIST_MORE": "{{areas}} und {{count}} weitere", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/en.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/en.json index 8994cd64..ad1f0068 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/en.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/en.json @@ -1,5 +1,6 @@ { "NAV": { + "PLACES": "Places", "GROUP_ALARMS": "Alarms", "GROUP_SETTINGS": "Settings", "GROUP_ADMIN": "Admin", @@ -63,8 +64,7 @@ "ACCENT_MYSTIC": "Mystic", "ACCENT_VALOR": "Valor", "ACCENT_INSTINCT": "Instinct", - "ALERT_DEFAULTS": "Alert Defaults", - "PLACES": "Places" + "ALERT_DEFAULTS": "Alert Defaults" }, "SHORTCUTS": { "TITLE": "Keyboard Shortcuts", @@ -775,6 +775,7 @@ "LEVEL_GMAX_LEGENDARY": "Legendary Gigantamax" }, "AREAS": { + "MANAGE_PLACES": "Manage places", "PAGE_TITLE": "Areas & Location", "PAGE_DESC": "Control where you receive notifications.", "METHOD_AREAS": "Areas", @@ -1722,6 +1723,7 @@ "LAST_PAGE": "Last page" }, "WHERE": { + "PLACES_PAGE_DESC": "Named points your alerts can be aimed at, instead of your pin.", "EDIT_FROM_CARD": "Change this from the card.", "MEASURED_FROM": "Measured from", "SCOPE_SAVED": "Where updated.", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/es.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/es.json index 5e08c247..e7cf2f1a 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/es.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/es.json @@ -1,5 +1,6 @@ { "NAV": { + "PLACES": "Lugares", "GROUP_ALARMS": "Alarmas", "GROUP_SETTINGS": "Ajustes", "GROUP_ADMIN": "Admin", @@ -63,8 +64,7 @@ "ACCENT_MYSTIC": "Mystic", "ACCENT_VALOR": "Valor", "ACCENT_INSTINCT": "Instinct", - "ALERT_DEFAULTS": "Valores predeterminados de alertas", - "PLACES": "Lugares" + "ALERT_DEFAULTS": "Valores predeterminados de alertas" }, "SHORTCUTS": { "TITLE": "Atajos de teclado", @@ -775,6 +775,7 @@ "SNACK_UPDATED": "Alarma de Combate Dinamax actualizada" }, "AREAS": { + "MANAGE_PLACES": "Gestionar lugares", "PAGE_TITLE": "Zonas y ubicación", "PAGE_DESC": "Controla dónde recibes las notificaciones.", "METHOD_AREAS": "Zonas", @@ -1722,6 +1723,7 @@ "LAST_PAGE": "Última página" }, "WHERE": { + "PLACES_PAGE_DESC": "Puntos con nombre a los que puedes dirigir tus alertas en vez de a tu ubicación.", "ADD_PLACE": "Añadir un lugar", "AREAS_LABEL": "Áreas", "AREA_LIST_MORE": "{{areas}} y {{count}} más", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/fr.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/fr.json index 8944af44..371368b5 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/fr.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/fr.json @@ -1,5 +1,6 @@ { "NAV": { + "PLACES": "Lieux", "GROUP_ALARMS": "Alarmes", "GROUP_SETTINGS": "Paramètres", "GROUP_ADMIN": "Admin", @@ -63,8 +64,7 @@ "ACCENT_MYSTIC": "Mystic", "ACCENT_VALOR": "Valor", "ACCENT_INSTINCT": "Instinct", - "ALERT_DEFAULTS": "Réglages par défaut des alertes", - "PLACES": "Lieux" + "ALERT_DEFAULTS": "Réglages par défaut des alertes" }, "SHORTCUTS": { "TITLE": "Raccourcis clavier", @@ -775,6 +775,7 @@ "SNACK_UPDATED": "Alarme Combat Dynamax mise à jour" }, "AREAS": { + "MANAGE_PLACES": "Gérer les lieux", "PAGE_TITLE": "Zones et localisation", "PAGE_DESC": "Contrôle où tu reçois les notifications.", "METHOD_AREAS": "Zones", @@ -1722,6 +1723,7 @@ "LAST_PAGE": "Dernière page" }, "WHERE": { + "PLACES_PAGE_DESC": "Des points nommés vers lesquels diriger vos alertes, au lieu de votre position.", "ADD_PLACE": "Ajouter un lieu", "AREAS_LABEL": "Zones", "AREA_LIST_MORE": "{{areas}} et {{count}} autres", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/it.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/it.json index d79dd5a3..20642ce1 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/it.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/it.json @@ -1,5 +1,6 @@ { "NAV": { + "PLACES": "Luoghi", "GROUP_ALARMS": "Allarmi", "GROUP_SETTINGS": "Impostazioni", "GROUP_ADMIN": "Amministrazione", @@ -63,8 +64,7 @@ "ACCENT_MYSTIC": "Mystic", "ACCENT_VALOR": "Valor", "ACCENT_INSTINCT": "Instinct", - "ALERT_DEFAULTS": "Impostazioni predefinite avvisi", - "PLACES": "Luoghi" + "ALERT_DEFAULTS": "Impostazioni predefinite avvisi" }, "SHORTCUTS": { "TITLE": "Scorciatoie da Tastiera", @@ -775,6 +775,7 @@ "SNACK_UPDATED": "Allarme Lotta Dynamax aggiornato" }, "AREAS": { + "MANAGE_PLACES": "Gestisci i luoghi", "PAGE_TITLE": "Aree e Posizione", "PAGE_DESC": "Controlla dove ricevi le notifiche.", "METHOD_AREAS": "Aree", @@ -1722,6 +1723,7 @@ "LAST_PAGE": "Ultima pagina" }, "WHERE": { + "PLACES_PAGE_DESC": "Punti con un nome a cui puntare i tuoi avvisi, al posto della tua posizione.", "ADD_PLACE": "Aggiungi un luogo", "AREAS_LABEL": "Aree", "AREA_LIST_MORE": "{{areas}} e altre {{count}}", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/nl.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/nl.json index cc8ed20c..5d6a44e6 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/nl.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/nl.json @@ -1,5 +1,6 @@ { "NAV": { + "PLACES": "Plaatsen", "GROUP_ALARMS": "Alarmen", "GROUP_SETTINGS": "Instellingen", "GROUP_ADMIN": "Beheer", @@ -63,8 +64,7 @@ "ACCENT_MYSTIC": "Mystic", "ACCENT_VALOR": "Valor", "ACCENT_INSTINCT": "Instinct", - "ALERT_DEFAULTS": "Standaardinstellingen meldingen", - "PLACES": "Plaatsen" + "ALERT_DEFAULTS": "Standaardinstellingen meldingen" }, "SHORTCUTS": { "TITLE": "Sneltoetsen", @@ -775,6 +775,7 @@ "SNACK_UPDATED": "Max Battle-alarm bijgewerkt" }, "AREAS": { + "MANAGE_PLACES": "Plaatsen beheren", "PAGE_TITLE": "Gebieden & Locatie", "PAGE_DESC": "Bepaal waar je meldingen ontvangt.", "METHOD_AREAS": "Gebieden", @@ -1722,6 +1723,7 @@ "LAST_PAGE": "Laatste pagina" }, "WHERE": { + "PLACES_PAGE_DESC": "Benoemde punten waar je meldingen op gericht kunnen worden, in plaats van je locatie.", "ADD_PLACE": "Plaats toevoegen", "AREAS_LABEL": "Gebieden", "AREA_LIST_MORE": "{{areas}} en nog {{count}}", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pl.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pl.json index 19f4f12b..81dfed11 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pl.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pl.json @@ -1,5 +1,6 @@ { "NAV": { + "PLACES": "Miejsca", "GROUP_ALARMS": "Alarmy", "GROUP_SETTINGS": "Ustawienia", "GROUP_ADMIN": "Admin", @@ -63,8 +64,7 @@ "ACCENT_MYSTIC": "Mystic", "ACCENT_VALOR": "Valor", "ACCENT_INSTINCT": "Instinct", - "ALERT_DEFAULTS": "Domyślne ustawienia alertów", - "PLACES": "Miejsca" + "ALERT_DEFAULTS": "Domyślne ustawienia alertów" }, "SHORTCUTS": { "TITLE": "Skróty klawiszowe", @@ -775,6 +775,7 @@ "SNACK_UPDATED": "Zaktualizowano alarm Max Battle" }, "AREAS": { + "MANAGE_PLACES": "Zarządzaj miejscami", "PAGE_TITLE": "Obszary i lokalizacja", "PAGE_DESC": "Kontroluj, gdzie otrzymujesz powiadomienia.", "METHOD_AREAS": "Obszary", @@ -1722,6 +1723,7 @@ "LAST_PAGE": "Ostatnia strona" }, "WHERE": { + "PLACES_PAGE_DESC": "Nazwane punkty, na które możesz kierować alerty zamiast na swoją lokalizację.", "ADD_PLACE": "Dodaj miejsce", "AREAS_LABEL": "Obszary", "AREA_LIST_MORE": "{{areas}} i jeszcze {{count}}", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt-BR.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt-BR.json index bf0627a3..da02ccc4 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt-BR.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt-BR.json @@ -1,5 +1,6 @@ { "NAV": { + "PLACES": "Locais", "GROUP_ALARMS": "Alarmes", "GROUP_SETTINGS": "Configurações", "GROUP_ADMIN": "Admin", @@ -63,8 +64,7 @@ "ACCENT_MYSTIC": "Mystic", "ACCENT_VALOR": "Valor", "ACCENT_INSTINCT": "Instinct", - "ALERT_DEFAULTS": "Padrões de alertas", - "PLACES": "Locais" + "ALERT_DEFAULTS": "Padrões de alertas" }, "SHORTCUTS": { "TITLE": "Atalhos do Teclado", @@ -775,6 +775,7 @@ "SNACK_UPDATED": "Alarme de Batalha Max atualizado" }, "AREAS": { + "MANAGE_PLACES": "Gerenciar locais", "PAGE_TITLE": "Áreas e Localização", "PAGE_DESC": "Controle onde você recebe notificações.", "METHOD_AREAS": "Áreas", @@ -1722,6 +1723,7 @@ "LAST_PAGE": "Última página" }, "WHERE": { + "PLACES_PAGE_DESC": "Pontos nomeados para onde direcionar seus alertas, em vez da sua localização.", "ADD_PLACE": "Adicionar um local", "AREAS_LABEL": "Áreas", "AREA_LIST_MORE": "{{areas}} e mais {{count}}", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt.json index 39ae39ea..950d530a 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/pt.json @@ -1,5 +1,6 @@ { "NAV": { + "PLACES": "Locais", "GROUP_ALARMS": "Alarmes", "GROUP_SETTINGS": "Definições", "GROUP_ADMIN": "Administração", @@ -63,8 +64,7 @@ "ACCENT_MYSTIC": "Mystic", "ACCENT_VALOR": "Valor", "ACCENT_INSTINCT": "Instinct", - "ALERT_DEFAULTS": "Padrões de alertas", - "PLACES": "Locais" + "ALERT_DEFAULTS": "Padrões de alertas" }, "SHORTCUTS": { "TITLE": "Atalhos de Teclado", @@ -775,6 +775,7 @@ "SNACK_UPDATED": "Alarme de Combate Max atualizado" }, "AREAS": { + "MANAGE_PLACES": "Gerir locais", "PAGE_TITLE": "Áreas e Localização", "PAGE_DESC": "Controla onde recebes notificações.", "METHOD_AREAS": "Áreas", @@ -1722,6 +1723,7 @@ "LAST_PAGE": "Última página" }, "WHERE": { + "PLACES_PAGE_DESC": "Pontos com nome para onde podes dirigir os teus alertas, em vez da tua localização.", "ADD_PLACE": "Adicionar um local", "AREAS_LABEL": "Áreas", "AREA_LIST_MORE": "{{areas}} e mais {{count}}", diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/sv.json b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/sv.json index a373cce0..d93470f7 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/sv.json +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/assets/i18n/sv.json @@ -1,5 +1,6 @@ { "NAV": { + "PLACES": "Platser", "GROUP_ALARMS": "Larm", "GROUP_SETTINGS": "Inställningar", "GROUP_ADMIN": "Admin", @@ -63,8 +64,7 @@ "ACCENT_MYSTIC": "Mystic", "ACCENT_VALOR": "Valor", "ACCENT_INSTINCT": "Instinct", - "ALERT_DEFAULTS": "Standardinställningar för aviseringar", - "PLACES": "Platser" + "ALERT_DEFAULTS": "Standardinställningar för aviseringar" }, "SHORTCUTS": { "TITLE": "Kortkommandon", @@ -775,6 +775,7 @@ "SNACK_UPDATED": "Max Battle-larm uppdaterat" }, "AREAS": { + "MANAGE_PLACES": "Hantera platser", "PAGE_TITLE": "Områden och plats", "PAGE_DESC": "Kontrollera var du tar emot notiser.", "METHOD_AREAS": "Områden", @@ -1722,6 +1723,7 @@ "LAST_PAGE": "Sista sidan" }, "WHERE": { + "PLACES_PAGE_DESC": "Namngivna punkter som dina aviseringar kan riktas mot i stället för din position.", "ADD_PLACE": "Lägg till en plats", "AREAS_LABEL": "Områden", "AREA_LIST_MORE": "{{areas}} och {{count}} till", diff --git a/CHANGELOG.md b/CHANGELOG.md index c0e82f81..1bf2805c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Places is a page in the sidebar, beside Areas and My Geofences.** It was a dialog in the user menu, which put your data next to your preferences and split one idea across two screens: the Areas page already explains that alerts reach you by area or by a radius from a point, and named points are more of the second. The Areas page's location card now links to it. Reachable only where the location feature is enabled, matching the endpoints it calls ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). + - **New alarms can start somewhere other than your pin.** Alert Defaults gains a place alongside the radius, and all ten add dialogs now offer a "measured from" selector, so a preference like "within 2 km of work" applies to everything you create instead of being re-picked each time. A place that gets deleted is forgotten rather than left seeding alarms with a label PoracleNG rejects ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). - **PVP rules can target mega evolutions.** PoracleNG 5.1.0 ranks megas separately from base forms, so a Pokemon PVP rule now chooses between Base, Mega, Mega X and Mega Y, and the card's PVP badge says which. Works regardless of the server's `include_mega_evolution` setting, which only decides the default for rules that do not ask ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). From 1180fd89e1c1f4bcd778ea47539ded22d7c1b2db Mon Sep 17 00:00:00 2001 From: hokiepokedad2 <38219945+hokiepokedad2@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:12:05 -0400 Subject: [PATCH 3/3] docs: fold the superseded Places entry into the accurate one Both described the same unreleased feature and disagreed about where it lives. Nothing has shipped, so the log should say where Places is rather than record the route it took. --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a8dad2d..3048e1bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,14 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Places is a page in the sidebar, beside Areas and My Geofences.** It was a dialog in the user menu, which put your data next to your preferences and split one idea across two screens: the Areas page already explains that alerts reach you by area or by a radius from a point, and named points are more of the second. The Areas page's location card now links to it. Reachable only where the location feature is enabled, matching the endpoints it calls ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). +- **Places.** A page in the sidebar beside Areas and My Geofences: your pin, plus whatever points you name. Adding one borrows the existing location map as a picker rather than growing a second one, and naming is a separate step because picking a point and naming it are two decisions. Deleting a place that alerts still point at says how many and refuses, instead of quietly widening them back to your pin. The Areas page's location card links to it, since that page already explains that alerts reach you by area or by a radius from a point and named points are more of the second. Shown only where the location feature is enabled, matching the endpoints it calls ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). - **New alarms can start somewhere other than your pin.** Alert Defaults gains a place alongside the radius, and all ten add dialogs now offer a "measured from" selector, so a preference like "within 2 km of work" applies to everything you create instead of being re-picked each time. A place that gets deleted is forgotten rather than left seeding alarms with a label PoracleNG rejects ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). - **PVP rules can target mega evolutions.** PoracleNG 5.1.0 ranks megas separately from base forms, so a Pokemon PVP rule now chooses between Base, Mega, Mega X and Mega Y, and the card's PVP badge says which. Works regardless of the server's `include_mega_evolution` setting, which only decides the default for rules that do not ask ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). - **Every alarm type says where it reaches you, in your own language.** The scope chip now appears on raid, egg, quest, invasion, lure, nest, gym, fort-change and max-battle cards as well as Pokemon, and clicking it opens the same picker. Raids, eggs, quests and max battles got it through the shared alarm-info summary, so four types describe themselves correctly from one implementation rather than four. Edit dialogs show the scope read-only and hide the areas-or-distance control for an area-confined alarm, which that control cannot describe and a save would have had refused. All 37 strings are translated into the ten non-English locales ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). - **Alarms can be aimed somewhere other than your pin.** PoracleNG 5.1.0 gives every alarm its own delivery scope, and the API now carries it: an alarm can measure its radius from a saved place ("within 2 km of work") or be confined to a set of areas, instead of inheriting the profile's single pin and area list. Saved places are managed at `GET/POST /api/location/places` and `DELETE /api/location/places/{label}`; deleting a place that alarms still point at answers 409 and names them rather than orphaning the label. The three ways a scope can contradict itself — a place and areas together, areas with a radius, a place without one — are refused before anything is written, with wording that says which one to change ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). - **Your own geofences work as an alarm's areas.** PoracleNG refuses `override_areas` entries whose fence is not user-selectable, and PoracleWeb serves user-drawn geofences that way deliberately, to keep them out of the bot's area picker — so naming one would have failed the whole write with "area not permitted". Matching never consults that flag, so the permitted names are sent to PoracleNG and the full list is written to the alarm afterwards, then state is reloaded. Verified against PoracleNG 5.1.0's matcher rather than inferred. Tagged `HACK: trusted-set-areas` alongside the existing area workarounds, and removable in one piece if PoracleNG grows a trusted override write ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). - **Every alarm can say where it should reach you.** The scope an alarm has always had, inherited and invisible, is now stated on the card and editable per alarm: anywhere in your areas, within a radius of a saved place, or only in specific areas. One shared control reads and writes it wherever it appears, rather than two more fields in each of the nine alarm dialogs. The three options are a radio group because PoracleNG treats them as mutually exclusive, so the combinations it refuses cannot be expressed in the first place ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). -- **Places.** The user menu gains a Places screen: your pin, plus whatever you name. Adding one borrows the existing location map as a picker rather than growing a second one, and naming is a separate step because picking a point and naming it are two decisions. Deleting a place that alerts still point at says how many and refuses, instead of quietly widening them back to your pin ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). - **The Pokemon card says where each alert reaches you, and lets you change it there.** The chip replaces the old areas-or-distance badge and covers the cases it could not say: within a radius of a saved place, or confined to specific areas. Clicking it opens the scope picker without opening the whole edit dialog. The other nine alarm types follow ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)). - **New Pokemon alarms can be aimed at a saved place.** The delivery step's radius gains a "measured from" selector: your pin, as before, or any place you have saved. Editing an alarm shows its scope but sends you to the card to change it, so there is one way to do it rather than two that can disagree ([#730](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/730)).