From 8cb47ff9d1a9adb1fffb6b94687c215085e38c61 Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 29 Sep 2025 15:37:49 -0700 Subject: [PATCH 01/26] feat: tappables --- config/default.json | 42 +++++- packages/locales/lib/human/en.json | 1 + packages/types/lib/scanner.d.ts | 15 ++ packages/types/lib/server.d.ts | 3 + server/src/filters/builder/base.js | 9 ++ server/src/filters/builder/tappable.js | 29 ++++ server/src/graphql/resolvers.js | 8 + server/src/graphql/typeDefs/index.graphql | 8 + server/src/graphql/typeDefs/scanner.graphql | 12 ++ server/src/models/Tappable.js | 129 ++++++++++++++++ server/src/models/index.js | 2 + server/src/routes/api/v1/available.js | 7 + server/src/routes/rootRouter.js | 3 + server/src/services/DbManager.js | 1 + server/src/services/EventManager.js | 1 + server/src/services/state.js | 1 + server/src/ui/advMenus.js | 9 ++ server/src/ui/clientOptions.js | 47 ++++++ server/src/ui/drawer.js | 6 + src/assets/css/main.css | 50 ++++++ src/features/drawer/Extras.jsx | 3 + src/features/drawer/Tappables.jsx | 19 +++ src/features/drawer/components/Section.jsx | 1 + .../drawer/components/SelectorList.jsx | 2 + src/features/tappable/TappablePopup.jsx | 110 ++++++++++++++ src/features/tappable/TappableTile.jsx | 142 ++++++++++++++++++ src/features/tappable/index.js | 4 + src/hooks/useOpacity.js | 2 +- src/pages/map/components/QueryData.jsx | 1 + src/pages/map/hooks/usePermCheck.js | 5 + src/pages/map/tileObject.js | 2 + src/services/Assets.js | 13 ++ src/services/queries/available.js | 6 + src/services/queries/index.js | 5 + src/services/queries/tappable.js | 30 ++++ src/store/useMemory.js | 5 + src/store/useStorage.js | 2 + 37 files changed, 731 insertions(+), 4 deletions(-) create mode 100644 server/src/filters/builder/tappable.js create mode 100644 server/src/models/Tappable.js create mode 100644 src/features/drawer/Tappables.jsx create mode 100644 src/features/tappable/TappablePopup.jsx create mode 100644 src/features/tappable/TappableTile.jsx create mode 100644 src/features/tappable/index.js create mode 100644 src/services/queries/tappable.js diff --git a/config/default.json b/config/default.json index 7448f596d..50c3a5c5b 100644 --- a/config/default.json +++ b/config/default.json @@ -64,7 +64,8 @@ "quests": false, "raids": true, "nests": false, - "stations": false + "stations": false, + "tappables": false }, "dataRequestLimits": { "categories": { @@ -76,7 +77,8 @@ "portals": 0, "routes": 0, "weather": 0, - "stations": 0 + "stations": 0, + "tappables": 0 }, "time": 60 }, @@ -89,7 +91,8 @@ "spawnpoints": 10000, "nests": 2500, "scanCells": 5000, - "stations": 5000 + "stations": 5000, + "tappables": 5000 }, "pvp": { "leagues": [ @@ -117,6 +120,7 @@ "portalUpdateLimit": 30, "weatherCellLimit": 3, "stationUpdateLimit": 9, + "tappableUpdateLimit": 6, "stationInactiveLimitDays": 120, "searchResultsLimit": 15, "searchSoftKmLimit": 10, @@ -158,6 +162,7 @@ "gyms", "nests", "pokestops", + "tappables", "stations", "pokemon", "routes", @@ -266,6 +271,7 @@ "enablePokestopPopupCoordsSelector": false, "enablePortalPopupCoordsSelector": false, "enableStationPopupCoordsSelector": false, + "enableTappablePopupCoordsSelector": false, "customFloatingIcons": [], "expandAllScanAreas": false, "enableRouteDownload": false @@ -299,6 +305,10 @@ "stations": { "zoomLevel": 14, "forcedLimit": 2500 + }, + "tappables": { + "zoomLevel": 14, + "forcedLimit": 2500 } }, "messageOfTheDay": { @@ -389,6 +399,17 @@ "opacityFiveMinutes": 0.5, "opacityOneMinute": 0.25 }, + "tappables": { + "clustering": true, + "tappableTimers": false, + "interactionRanges": false, + "customRange": 0, + "tappablesOpacity": true, + "enableTappablePopupCoords": false, + "opacityTenMinutes": 0.75, + "opacityFiveMinutes": 0.5, + "opacityOneMinute": 0.25 + }, "pokemon": { "clustering": true, "pokemonTimers": false, @@ -536,6 +557,10 @@ "enabled": true } }, + "tappables": { + "enabled": false, + "items": true + }, "pokemon": { "enabled": false, "easyMode": true, @@ -790,6 +815,11 @@ "trialPeriodEligible": false, "roles": [] }, + "tappables": { + "enabled": true, + "trialPeriodEligible": false, + "roles": [] + }, "lures": { "enabled": true, "trialPeriodEligible": false, @@ -1017,6 +1047,12 @@ "md": 25, "lg": 35, "xl": 45 + }, + "tappable": { + "sm": 15, + "md": 25, + "lg": 35, + "xl": 45 } } }, diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index 3f79bacf1..ffbc1996b 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -805,6 +805,7 @@ "exclude_battle": "Exclude Max Battle", "station": "Power Spot", "stations": "Power Spots", + "tappables": "Tappables", "stations_filters": "Power Spots Filter Settings", "stations_options": "Power Spot Options", "all_stations": "All Power Spots", diff --git a/packages/types/lib/scanner.d.ts b/packages/types/lib/scanner.d.ts index 685a9343c..82f013768 100644 --- a/packages/types/lib/scanner.d.ts +++ b/packages/types/lib/scanner.d.ts @@ -10,6 +10,7 @@ import SpawnpointModel = require('server/src/models/Spawnpoint') import WeatherModel = require('server/src/models/Weather') import RouteModel = require('server/src/models/Route') import StationModel = require('server/src/models/Station') +import TappableModel = require('server/src/models/Tappable') import { S2Polygon } from './general' @@ -274,6 +275,20 @@ export interface Portal { export type FullPortal = FullModel +export interface Tappable { + id: string + lat: number + lon: number + type: string + item_id: number + count: number | null + expire_timestamp: number | null + expire_timestamp_verified: boolean + updated: number +} + +export type FullTappable = FullModel + export interface ScanCell { id?: string level?: number diff --git a/packages/types/lib/server.d.ts b/packages/types/lib/server.d.ts index e4211fdd9..b0ab8ef42 100644 --- a/packages/types/lib/server.d.ts +++ b/packages/types/lib/server.d.ts @@ -5,6 +5,7 @@ import type { RmModelKeys, ModelKeys, Station, + Tappable, Backup, Nest, NestSubmission, @@ -70,6 +71,7 @@ export interface Available { pokestops: ModelReturn nests: ModelReturn stations: ModelReturn + tappables: ModelReturn } export interface ApiEndpoint { @@ -204,6 +206,7 @@ export type AdvCategories = | 'pokestops' | 'nests' | 'stations' + | 'tappables' export type UIObject = ReturnType< (typeof import('server/src/ui/drawer'))['drawer'] diff --git a/server/src/filters/builder/base.js b/server/src/filters/builder/base.js index ab8b7fc85..e5c865a8b 100644 --- a/server/src/filters/builder/base.js +++ b/server/src/filters/builder/base.js @@ -5,6 +5,7 @@ const { state } = require('../../services/state') const { buildPokemon } = require('./pokemon') const { buildPokestops } = require('./pokestop') const { buildGyms } = require('./gym') +const { buildTappables } = require('./tappable') const { BaseFilter } = require('../Base') const { PokemonFilter } = require('../pokemon/Frontend') @@ -111,6 +112,14 @@ function buildDefaultFilters(perms) { }, } : undefined, + tappables: + perms.tappables && state.db.models.Tappable + ? { + enabled: defaultFilters.tappables.enabled, + standard: new BaseFilter(), + filter: buildTappables(perms, defaultFilters.tappables), + } + : undefined, stations: stationReducer && state.db.models.Station ? { diff --git a/server/src/filters/builder/tappable.js b/server/src/filters/builder/tappable.js new file mode 100644 index 000000000..554845f62 --- /dev/null +++ b/server/src/filters/builder/tappable.js @@ -0,0 +1,29 @@ +// @ts-check +const { state } = require('../../services/state') +const { BaseFilter } = require('../Base') + +/** + * @param {import('@rm/types').Permissions} perms + * @param {import('@rm/types').Config['defaultFilters']['tappables']} defaults + * @returns {Record} + */ +function buildTappables(perms, defaults) { + const filters = { s0: new BaseFilter() } + if (!perms.tappables) { + return filters + } + + Object.keys(state.event.masterfile.items).forEach((itemId) => { + filters[`q${itemId}`] = new BaseFilter(defaults.items) + }) + + state.event.getAvailable('tappables').forEach((key) => { + if (!filters[key]) { + filters[key] = new BaseFilter(defaults.items) + } + }) + + return filters +} + +module.exports = { buildTappables } diff --git a/server/src/graphql/resolvers.js b/server/src/graphql/resolvers.js index a0b4705bf..afc4982a9 100644 --- a/server/src/graphql/resolvers.js +++ b/server/src/graphql/resolvers.js @@ -92,6 +92,8 @@ const resolvers = { }), availableStations: (_, _args, { Event, perms }) => perms?.dynamax ? Event.available.stations : [], + availableTappables: (_, _args, { Event, perms }) => + perms?.tappables ? Event.available.tappables : [], backup: (_, args, { req, perms, Db }) => { if (perms?.backups && req?.user?.id) { return Db.models.Backup.getOne(args.id, req?.user?.id) @@ -526,6 +528,12 @@ const resolvers = { } return [] }, + tappables: (_, args, { perms, Db }) => { + if (perms?.tappables) { + return Db.query('Tappable', 'getAll', perms, args) + } + return [] + }, submissionCells: async (_, args, { req, perms, Db }) => { const { submissionZoom } = config.getMapConfig(req).general if (perms?.submissionCells && args.zoom >= submissionZoom - 1) { diff --git a/server/src/graphql/typeDefs/index.graphql b/server/src/graphql/typeDefs/index.graphql index e2bd1aaa2..8dc658947 100644 --- a/server/src/graphql/typeDefs/index.graphql +++ b/server/src/graphql/typeDefs/index.graphql @@ -7,6 +7,7 @@ type Query { availableGyms: [String] availableNests: [String] availableStations: [String] + availableTappables: [String] badges: [Badge] backup(id: ID): Backup backups: [Backup] @@ -67,6 +68,13 @@ type Query { filters: JSON ): [Station] stationPokemon(id: ID): [StationPokemon] + tappables( + minLat: Float + maxLat: Float + minLon: Float + maxLon: Float + filters: JSON + ): [Tappable] s2cells( minLat: Float maxLat: Float diff --git a/server/src/graphql/typeDefs/scanner.graphql b/server/src/graphql/typeDefs/scanner.graphql index b28474aef..f07e6bbae 100644 --- a/server/src/graphql/typeDefs/scanner.graphql +++ b/server/src/graphql/typeDefs/scanner.graphql @@ -201,6 +201,18 @@ type Portal { updated: Int } +type Tappable { + id: ID + lat: Float + lon: Float + type: String + item_id: Int + count: Int + expire_timestamp: Int + expire_timestamp_verified: Boolean + updated: Int +} + type ScanCell { id: ID level: Int diff --git a/server/src/models/Tappable.js b/server/src/models/Tappable.js new file mode 100644 index 000000000..09f4e4352 --- /dev/null +++ b/server/src/models/Tappable.js @@ -0,0 +1,129 @@ +// @ts-check +const { Model } = require('objection') +const config = require('@rm/config') + +const { getAreaSql } = require('../utils/getAreaSql') +const { applyManualIdFilter } = require('../utils/manualFilter') +const { getEpoch } = require('../utils/getClientTime') + +class Tappable extends Model { + static get tableName() { + return 'tappable' + } + + /** + * @param {import('@rm/types').Permissions} perms + * @param {{ + * filters: Record, + * minLat: number, + * maxLat: number, + * minLon: number, + * maxLon: number, + * }} args + * @param {import('@rm/types').DbContext} ctx + * @returns {Promise} + */ + static async getAll(perms, args, ctx) { + if (!perms?.tappables) return [] + + const { filters: filterArgs = {}, minLat, maxLat, minLon, maxLon } = args + + const { queryLimits = {}, tappableUpdateLimit = 6 } = config.getSafe('api') + const timestamp = getEpoch() + + const query = this.query().select([ + 'id', + 'lat', + 'lon', + 'type', + 'item_id', + 'count', + 'expire_timestamp', + 'expire_timestamp_verified', + 'updated', + ]) + + applyManualIdFilter(query, { + manualId: filterArgs.onlyManualId, + latColumn: 'lat', + lonColumn: 'lon', + idColumn: 'id', + bounds: { minLat, maxLat, minLon, maxLon }, + }) + + const onlyAreas = filterArgs.onlyAreas || [] + if (!getAreaSql(query, perms.areaRestrictions, onlyAreas, ctx?.isMad)) { + return [] + } + + query.whereNull('pokemon_id').whereNotNull('item_id') + + query.andWhere((builder) => { + builder + .whereNull('expire_timestamp') + .orWhere('expire_timestamp', '>', timestamp) + }) + + if (tappableUpdateLimit > 0) { + query.andWhere('updated', '>', timestamp - tappableUpdateLimit * 60 * 60) + } + + const itemIds = [] + Object.keys(filterArgs).forEach((key) => { + if (!key || key.startsWith('only')) return + switch (key.charAt(0)) { + case 'q': { + const itemId = Number.parseInt(key.slice(1), 10) + if (!Number.isNaN(itemId)) { + itemIds.push(itemId) + } + break + } + case 's': + break + default: + break + } + }) + + if (itemIds.length) { + query.whereIn('item_id', itemIds) + } + + query.orderBy('updated', 'desc') + + const limit = queryLimits.tappables || queryLimits.pokestops || 5000 + const results = await query.limit(limit) + + return results.map((row) => ({ + ...row, + expire_timestamp_verified: !!row.expire_timestamp_verified, + })) + } + + /** + * Returns filter keys available for tappables + * @returns {Promise<{ available: string[] }>} + */ + static async getAvailable() { + const rows = await this.query() + .select('item_id') + .count('id as total') + .whereNull('pokemon_id') + .whereNotNull('item_id') + .groupBy('item_id') + + const available = Array.from( + new Set( + rows + .map((row) => row.item_id) + .filter((itemId) => itemId !== null) + .map((itemId) => `q${itemId}`), + ), + ) + + return { available } + } +} + +module.exports = { Tappable } diff --git a/server/src/models/index.js b/server/src/models/index.js index 3dd967fae..4e0da69f9 100644 --- a/server/src/models/index.js +++ b/server/src/models/index.js @@ -14,6 +14,7 @@ const { ScanCell } = require('./ScanCell') const { Session } = require('./Session') const { Spawnpoint } = require('./Spawnpoint') const { Station } = require('./Station') +const { Tappable } = require('./Tappable') const { User } = require('./User') const { Weather } = require('./Weather') @@ -36,6 +37,7 @@ const scannerModels = { ScanCell, Spawnpoint, Station, + Tappable, Weather, } diff --git a/server/src/routes/api/v1/available.js b/server/src/routes/api/v1/available.js index e79d1dd89..c0cb7298f 100644 --- a/server/src/routes/api/v1/available.js +++ b/server/src/routes/api/v1/available.js @@ -9,6 +9,7 @@ const queryObj = /** @type {const} */ ({ quests: { model: 'Pokestop', category: 'pokestops' }, raids: { model: 'Gym', category: 'gyms' }, nests: { model: 'Nest', category: 'nests' }, + tappables: { model: 'Tappable', category: 'tappables' }, }) /** @param {string} category */ @@ -27,6 +28,9 @@ const resolveCategory = (category) => { case 'pokemon': case 'pokemons': return 'pokemon' + case 'tappable': + case 'tappables': + return 'tappables' default: return 'all' } @@ -40,12 +44,14 @@ const getAll = async (compare) => { state.db.getAvailable('Pokestop'), state.db.getAvailable('Gym'), state.db.getAvailable('Nest'), + state.db.getAvailable('Tappable'), ]) : [ state.event.available.pokemon, state.event.available.pokestops, state.event.available.gyms, state.event.available.nests, + state.event.available.tappables, ] return Object.fromEntries( Object.keys(queryObj).map((key, i) => [key, available[i]]), @@ -118,6 +124,7 @@ router.put('/:category', async (req, res) => { state.event.setAvailable('gyms', 'Gym', state.db), state.event.setAvailable('nests', 'Nest', state.db), state.event.setAvailable('stations', 'Station', state.db), + state.event.setAvailable('tappables', 'Tappable', state.db), ]) } res diff --git a/server/src/routes/rootRouter.js b/server/src/routes/rootRouter.js index a2feb1428..1d0bced5a 100644 --- a/server/src/routes/rootRouter.js +++ b/server/src/routes/rootRouter.js @@ -212,6 +212,9 @@ rootRouter.get('/api/settings', async (req, res, next) => { if (settings.user.perms.stations && api.queryOnSessionInit.stations) { state.event.setAvailable('stations', 'Station', state.db) } + if (settings.user.perms.tappables && api.queryOnSessionInit.tappables) { + state.event.setAvailable('tappables', 'Tappable', state.db) + } } res.status(200).json(settings) diff --git a/server/src/services/DbManager.js b/server/src/services/DbManager.js index ee1915074..066cbda30 100644 --- a/server/src/services/DbManager.js +++ b/server/src/services/DbManager.js @@ -24,6 +24,7 @@ class DbManager extends Logger { 'ScanCell', 'Spawnpoint', 'Station', + 'Tappable', 'Weather', ]) diff --git a/server/src/services/EventManager.js b/server/src/services/EventManager.js index 0bd7f4ae4..1eb5091bb 100644 --- a/server/src/services/EventManager.js +++ b/server/src/services/EventManager.js @@ -30,6 +30,7 @@ class EventManager extends Logger { pokemon: [], nests: [], stations: [], + tappables: [], }) this.uicons = getCache('uicons.json', []) this.uaudio = getCache('uaudio.json', []) diff --git a/server/src/services/state.js b/server/src/services/state.js index a1e34d87f..ad78e06a6 100644 --- a/server/src/services/state.js +++ b/server/src/services/state.js @@ -105,6 +105,7 @@ const state = { this.event.setAvailable('pokemon', 'Pokemon', this.db), this.event.setAvailable('nests', 'Nest', this.db), this.event.setAvailable('stations', 'Station', this.db), + this.event.setAvailable('tappables', 'Tappable', this.db), ) } await Promise.all(promises) diff --git a/server/src/ui/advMenus.js b/server/src/ui/advMenus.js index 908bb273f..0487e8cb7 100644 --- a/server/src/ui/advMenus.js +++ b/server/src/ui/advMenus.js @@ -20,6 +20,7 @@ const CATEGORIES = /** @type {const} */ ({ stations: ['pokemon'], pokemon: ['pokemon'], nests: ['pokemon'], + tappables: ['items'], }) /** @@ -110,6 +111,14 @@ function advMenus(perms) { } : {}, }, + tappables: { + categories: CATEGORIES.tappables, + filters: { + categories: Object.fromEntries( + CATEGORIES.tappables.map((item) => [item, false]), + ), + }, + }, pokemon: { categories: CATEGORIES.pokemon, filters: { diff --git a/server/src/ui/clientOptions.js b/server/src/ui/clientOptions.js index 638c62ed1..868922823 100644 --- a/server/src/ui/clientOptions.js +++ b/server/src/ui/clientOptions.js @@ -171,6 +171,53 @@ function clientOptions(perms) { ? { type: 'bool', perm: ['stations', 'dynamax'], category: 'popups' } : undefined, }, + tappables: { + clustering: { + type: 'bool', + perm: ['tappables'], + category: 'markers', + }, + tappableTimers: { + type: 'bool', + perm: ['tappables'], + category: 'tooltips', + }, + interactionRanges: { + type: 'bool', + perm: ['tappables'], + category: 'markers', + }, + customRange: { + type: 'number', + perm: ['tappables'], + min: 0, + max: 5000, + category: 'markers', + }, + tappablesOpacity: { + type: 'bool', + perm: ['tappables'], + category: 'dynamic_opacity', + }, + opacityTenMinutes: { + type: 'number', + perm: ['tappables'], + category: 'dynamic_opacity', + }, + opacityFiveMinutes: { + type: 'number', + perm: ['tappables'], + category: 'dynamic_opacity', + }, + opacityOneMinute: { + type: 'number', + perm: ['tappables'], + category: 'dynamic_opacity', + }, + enableTappablePopupCoords: map.misc.enableTappablePopupCoordsSelector + ? { type: 'bool', perm: ['tappables'], category: 'popups' } + : undefined, + }, pokemon: { clustering: { type: 'bool', perm: ['pokemon'], category: 'markers' }, linkGlobalAndAdvanced: { diff --git a/server/src/ui/drawer.js b/server/src/ui/drawer.js index 93c1b22b9..3af7527df 100644 --- a/server/src/ui/drawer.js +++ b/server/src/ui/drawer.js @@ -63,6 +63,12 @@ function drawer(req, perms) { arEligible: perms.pokestops || BLOCKED, } : BLOCKED, + tappables: + perms.tappables && state.db.models.Tappable + ? { + enabled: perms.tappables || BLOCKED, + } + : BLOCKED, stations: (perms.stations || perms.dynamax) && state.db.models.Station ? { diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 72c7badcd..dab88e6f0 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -347,6 +347,56 @@ img { filter: invert(75%) sepia(100%) hue-rotate(151deg) saturate(3.1); } +.tappable-marker { + position: relative; + width: var(--tappable-size, 32px); + height: var(--tappable-size, 32px); + display: flex; + align-items: center; + justify-content: center; +} + +.tappable-marker__icon { + width: 100%; + height: 100%; + object-fit: contain; +} + +.tappable-marker__bubble { + position: absolute; + bottom: calc(100% + 6px); + left: 50%; + transform: translateX(-50%); + display: inline-flex; + align-items: center; + gap: 4px; + background-color: rgba(0, 0, 0, 0.85); + color: #fff; + padding: 2px 6px; + border-radius: 12px; + font-size: 12px; + line-height: 1; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + white-space: nowrap; +} + +.tappable-marker__bubble::after { + content: ''; + position: absolute; + bottom: -6px; + left: 50%; + transform: translateX(-50%); + border-style: solid; + border-width: 6px 6px 0 6px; + border-color: rgba(0, 0, 0, 0.85) transparent transparent transparent; +} + +.tappable-marker__bubble img { + width: 20px; + height: 20px; + object-fit: contain; +} + .quest-popup-img { max-width: 30px; max-height: 30px; diff --git a/src/features/drawer/Extras.jsx b/src/features/drawer/Extras.jsx index 9ada71fcc..d427e17cc 100644 --- a/src/features/drawer/Extras.jsx +++ b/src/features/drawer/Extras.jsx @@ -9,6 +9,7 @@ import { WayfarerDrawer } from './Wayfarer' import { S2CellsDrawer } from './S2Cells' import { AdminDrawer } from './Admin' import { StationsDrawer } from './Stations' +import { TappablesDrawer } from './Tappables' function ExtrasComponent({ category, subItem }) { switch (category) { @@ -28,6 +29,8 @@ function ExtrasComponent({ category, subItem }) { return case 'stations': return subItem === 'maxBattles' && + case 'tappables': + return default: return null } diff --git a/src/features/drawer/Tappables.jsx b/src/features/drawer/Tappables.jsx new file mode 100644 index 000000000..0625db5a4 --- /dev/null +++ b/src/features/drawer/Tappables.jsx @@ -0,0 +1,19 @@ +// @ts-check +import * as React from 'react' + +import { useStorage } from '@store/useStorage' + +import { CollapsibleItem } from './components/CollapsibleItem' +import { SelectorListMemo } from './components/SelectorList' + +const BaseTappablesDrawer = () => { + const enabled = useStorage((s) => !!s.filters?.tappables?.enabled) + + return ( + + + + ) +} + +export const TappablesDrawer = React.memo(BaseTappablesDrawer) diff --git a/src/features/drawer/components/Section.jsx b/src/features/drawer/components/Section.jsx index e78274d48..749cd236e 100644 --- a/src/features/drawer/components/Section.jsx +++ b/src/features/drawer/components/Section.jsx @@ -28,6 +28,7 @@ const ADV_CATEGORIES = new Set([ 'pokestops', 'nests', 'stations', + 'tappables', ]) /** @param {{ category: keyof import('@rm/types').UIObject }} props */ diff --git a/src/features/drawer/components/SelectorList.jsx b/src/features/drawer/components/SelectorList.jsx index 2bf6addb6..393f341a0 100644 --- a/src/features/drawer/components/SelectorList.jsx +++ b/src/features/drawer/components/SelectorList.jsx @@ -92,6 +92,8 @@ function SelectorList({ category, subCategory, label, height = 400 }) { switch (category) { case 'gyms': return key.startsWith('t') + case 'tappables': + return key.startsWith('q') default: return Number.isInteger(Number(key.charAt(0))) } diff --git a/src/features/tappable/TappablePopup.jsx b/src/features/tappable/TappablePopup.jsx new file mode 100644 index 000000000..5cb74e571 --- /dev/null +++ b/src/features/tappable/TappablePopup.jsx @@ -0,0 +1,110 @@ +// @ts-check +import * as React from 'react' +import Grid from '@mui/material/Unstable_Grid2' +import Typography from '@mui/material/Typography' +import Divider from '@mui/material/Divider' +import { useTranslation } from 'react-i18next' + +import { useStorage } from '@store/useStorage' +import { useMemory } from '@store/useMemory' +import { Navigation } from '@components/popups/Navigation' +import { Coords } from '@components/popups/Coords' +import { TimeStamp } from '@components/popups/TimeStamps' + +/** + * @param {{ + * tappable: import('@rm/types').Tappable, + * rewardIcon: string, + * iconSize: number, + * }} props + */ +export function TappablePopup({ tappable, rewardIcon, iconSize }) { + const { t, i18n } = useTranslation() + const showCoords = useStorage( + (s) => !!s.userSettings.tappables?.enableTappablePopupCoords, + ) + const masterfile = useMemory((s) => s.masterfile) + + const count = tappable.count ?? 1 + const itemName = React.useMemo(() => { + if (i18n.exists(`item_${tappable.item_id}`)) { + return t(`item_${tappable.item_id}`) + } + return masterfile.items?.[tappable.item_id]?.name || `#${tappable.item_id}` + }, [t, i18n, masterfile.items, tappable.item_id]) + + const formattedType = React.useMemo(() => { + if (!tappable.type) return '' + const cleaned = tappable.type + .replace('TAPPABLE_TYPE_', '') + .replace(/_/g, ' ') + .toLowerCase() + const translationKey = `tappable_type_${cleaned.replace(/\s+/g, '_')}` + if (i18n.exists(translationKey)) { + return t(translationKey) + } + return cleaned + .split(' ') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' ') + }, [tappable.type, t, i18n]) + + return ( + + {rewardIcon && ( + + {itemName} + + )} + + {itemName} + {count > 1 && ( + + ×{count} + + )} + {formattedType && ( + + {formattedType} + + )} + + + + + {showCoords && ( + + + + )} + {(tappable.expire_timestamp || tappable.updated) && ( + + + + )} + {tappable.expire_timestamp && ( + + + {t('disappear_time')} + + + )} + {tappable.updated && ( + + {t('last_updated')} + + )} + + ) +} diff --git a/src/features/tappable/TappableTile.jsx b/src/features/tappable/TappableTile.jsx new file mode 100644 index 000000000..718209163 --- /dev/null +++ b/src/features/tappable/TappableTile.jsx @@ -0,0 +1,142 @@ +/* eslint-disable react/destructuring-assignment */ +// @ts-check +import * as React from 'react' +import { Marker, Popup } from 'react-leaflet' +import { divIcon } from 'leaflet' + +import { useMemory } from '@store/useMemory' +import { useStorage } from '@store/useStorage' +import { useManualPopupTracker } from '@hooks/useManualPopupTracker' +import { useForcePopup } from '@hooks/useForcePopup' +import { useMarkerTimer } from '@hooks/useMarkerTimer' +import { useOpacity } from '@hooks/useOpacity' +import { TooltipWrapper } from '@components/ToolTipWrapper' + +import { TappablePopup } from './TappablePopup' + +/** + * @param {import('@rm/types').Tappable} tappable + */ +const BaseTappableTile = (tappable) => { + const Icons = useMemory((s) => s.Icons) + const itemFilters = useStorage((s) => s.filters?.tappables?.filter || {}) + const showTimer = useStorage( + (s) => !!s.userSettings.tappables?.tappableTimers, + ) + + const [markerRef, setMarkerRef] = React.useState(null) + useForcePopup(tappable.id, markerRef) + useMarkerTimer(tappable.expire_timestamp || 0, markerRef) + const handlePopupOpen = useManualPopupTracker('tappables', tappable.id) + + const getOpacity = useOpacity('tappables') + const opacity = React.useMemo( + () => + tappable.expire_timestamp ? getOpacity(tappable.expire_timestamp) : 1, + [getOpacity, tappable.expire_timestamp], + ) + + const { icon, rewardIcon, size } = React.useMemo(() => { + if (!Icons || !tappable.item_id) { + return { icon: null, rewardIcon: '', size: 24 } + } + const filterKey = `q${tappable.item_id}` + const tappableSize = Icons.getSize('tappable', itemFilters[filterKey]?.size) + const tappableIcon = Icons.getTappable(tappable.type) + const tappableReward = Icons.getRewards( + 2, + tappable.item_id, + tappable.count || 1, + ) + if (!tappableIcon) { + return { icon: null, rewardIcon: '', size: tappableSize } + } + const [tappableMod, rewardMod] = Icons.getModifiers('tappable', 'reward') + const popupAnchor = [ + tappableMod?.popupX || 0, + tappableSize * -0.7 * (tappableMod?.offsetY || 1) + + (tappableMod?.popupY || 0), + ] + + const html = ` +
+ ${ + tappableReward + ? ` +
+ ${tappable.item_id} + ${ + tappable.count && tappable.count > 1 + ? `x${tappable.count}` + : '' + } +
` + : '' + } + ${tappable.type || ''} +
+ ` + + return { + size: tappableSize, + rewardIcon: tappableReward, + icon: divIcon({ + className: 'tappable-marker-icon', + iconAnchor: [tappableSize / 2, tappableSize / 2], + popupAnchor, + html, + }), + } + }, [ + Icons, + itemFilters, + tappable.type, + tappable.item_id, + tappable.count, + opacity, + ]) + + if (!Icons || !icon) { + return null + } + + const timers = React.useMemo( + () => (tappable.expire_timestamp ? [tappable.expire_timestamp] : []), + [tappable.expire_timestamp], + ) + + return ( + + + + + {showTimer && !!timers.length && ( + + )} + + ) +} + +export const TappableTile = React.memo( + BaseTappableTile, + (prev, next) => prev.id === next.id && prev.updated === next.updated, +) diff --git a/src/features/tappable/index.js b/src/features/tappable/index.js new file mode 100644 index 000000000..b49abd07f --- /dev/null +++ b/src/features/tappable/index.js @@ -0,0 +1,4 @@ +// @ts-check + +export * from './TappableTile' +export * from './TappablePopup' diff --git a/src/hooks/useOpacity.js b/src/hooks/useOpacity.js index ee4a4833d..671405527 100644 --- a/src/hooks/useOpacity.js +++ b/src/hooks/useOpacity.js @@ -5,7 +5,7 @@ import { useStorage } from '@store/useStorage' /** * Returns dynamic opacity based on timestamp - * @template {'pokemon' | 'gyms' | 'pokestops' | 'stations'} T + * @template {'pokemon' | 'gyms' | 'pokestops' | 'stations' | 'tappables'} T * @param {T} category * @param {T extends 'pokestops' ? 'invasion' : T extends 'gyms' ? 'raid' : never} [subCategory] * @returns diff --git a/src/pages/map/components/QueryData.jsx b/src/pages/map/components/QueryData.jsx index 938619def..72dde54af 100644 --- a/src/pages/map/components/QueryData.jsx +++ b/src/pages/map/components/QueryData.jsx @@ -43,6 +43,7 @@ const MANUAL_ID_CATEGORIES = new Set([ 'portals', 'stations', 'routes', + 'tappables', ]) /** diff --git a/src/pages/map/hooks/usePermCheck.js b/src/pages/map/hooks/usePermCheck.js index de13a9eff..8f8ed86b5 100644 --- a/src/pages/map/hooks/usePermCheck.js +++ b/src/pages/map/hooks/usePermCheck.js @@ -63,6 +63,11 @@ export function usePermCheck(category) { return true } break + case 'tappables': + if (filters?.enabled && perms?.tappables) { + return true + } + break default: if (filters?.enabled && perms[category]) { return true diff --git a/src/pages/map/tileObject.js b/src/pages/map/tileObject.js index 71ccdcc5e..df2c5b479 100644 --- a/src/pages/map/tileObject.js +++ b/src/pages/map/tileObject.js @@ -14,6 +14,7 @@ import { WayfarerTile as submissionCells } from '@features/wayfarer' import { ScanAreaTile as scanAreas } from '@features/scanArea' import { BaseCell as s2cells } from '@features/s2cell' import { StationTile as stations } from '@features/station' +import { TappableTile as tappables } from '@features/tappable' export const TILES = { devices, @@ -30,4 +31,5 @@ export const TILES = { s2cells, routes, stations, + tappables, } diff --git a/src/services/Assets.js b/src/services/Assets.js index 9a97bf922..d579d5a21 100644 --- a/src/services/Assets.js +++ b/src/services/Assets.js @@ -413,6 +413,19 @@ export class UAssets { } } + /** + * @param {string} [type] + * @returns {string} + */ + getTappable(type = '0') { + try { + return this[this.selected.tappable]?.class?.tappable(type) + } catch (e) { + console.error(`[${this.assetType.toUpperCase()}]`, e) + return `${this.fallback}/tappable/0.${this.fallbackExt}` + } + } + /** * * @param {string | number} [rewardType] diff --git a/src/services/queries/available.js b/src/services/queries/available.js index 5722a3e53..debe67076 100644 --- a/src/services/queries/available.js +++ b/src/services/queries/available.js @@ -44,3 +44,9 @@ export const GET_AVAILABLE_STATIONS = gql` availableStations } ` + +export const GET_AVAILABLE_TAPPABLES = gql` + query AvailableTappables { + availableTappables + } +` diff --git a/src/services/queries/index.js b/src/services/queries/index.js index c0bd1eaa9..c36a3a105 100644 --- a/src/services/queries/index.js +++ b/src/services/queries/index.js @@ -9,6 +9,7 @@ import * as searchIndex from './search' import * as webhookIndex from './webhook' import * as user from './user' import * as stationIndex from './station' +import * as tappableIndex from './tappable' import { GET_ALL_DEVICES } from './device' import { GET_ALL_SPAWNPOINTS } from './spawnpoint' import { GET_ALL_WEATHER } from './weather' @@ -150,6 +151,10 @@ export class Query { return stationIndex[query] } + static tappables() { + return tappableIndex.GET_ALL_TAPPABLES + } + /** @param {string} category */ static search(category) { const { perms } = useMemory.getState().auth diff --git a/src/services/queries/tappable.js b/src/services/queries/tappable.js new file mode 100644 index 000000000..f77cd231b --- /dev/null +++ b/src/services/queries/tappable.js @@ -0,0 +1,30 @@ +// @ts-check +import { gql } from '@apollo/client' + +export const GET_ALL_TAPPABLES = gql` + query GetTappables( + $minLat: Float! + $maxLat: Float! + $minLon: Float! + $maxLon: Float! + $filters: JSON + ) { + tappables( + minLat: $minLat + maxLat: $maxLat + minLon: $minLon + maxLon: $maxLon + filters: $filters + ) { + id + lat + lon + type + item_id + count + expire_timestamp + expire_timestamp_verified + updated + } + } +` diff --git a/src/store/useMemory.js b/src/store/useMemory.js index f886857ae..04faead03 100644 --- a/src/store/useMemory.js +++ b/src/store/useMemory.js @@ -58,6 +58,8 @@ import { create } from 'zustand' * pokestops: string[], * nests: string[], * stations: string[], + * tappables: string[], + * tappables: string[], * questConditions: Record, * } * manualParams: { @@ -130,6 +132,7 @@ export const useMemory = create(() => ({ pokestops: [], nests: [], stations: [], + tappables: [], questConditions: {}, }, Icons: null, @@ -161,6 +164,7 @@ export const useMemory = create(() => ({ pokestops: { count: 0, show: 0, total: 0 }, nests: { count: 0, show: 0, total: 0 }, stations: { count: 0, show: 0, total: 0 }, + tappables: { count: 0, show: 0, total: 0 }, }, advMenuFiltered: { gyms: [], @@ -168,6 +172,7 @@ export const useMemory = create(() => ({ pokemon: [], nests: [], stations: [], + tappables: [], }, })) diff --git a/src/store/useStorage.js b/src/store/useStorage.js index 98ffc04ff..c933c39f9 100644 --- a/src/store/useStorage.js +++ b/src/store/useStorage.js @@ -28,6 +28,7 @@ import { setDeep } from '@utils/setDeep' * pokestops: string, * nests: string, * stations: string, + * tappables: string, * } * searches: Record, * tabs: Record, @@ -145,6 +146,7 @@ export const useStorage = create( pokestops: 'categories', nests: 'others', stations: 'others', + tappables: 'categories', }, search: '', searchTab: '', From 3dd34c84a04acb01ecf17be62911ed1366d46b8a Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 29 Sep 2025 16:08:22 -0700 Subject: [PATCH 02/26] fix: padding and adv_categories --- src/features/drawer/components/Section.jsx | 1 - src/features/drawer/components/SelectorList.jsx | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/drawer/components/Section.jsx b/src/features/drawer/components/Section.jsx index 749cd236e..e78274d48 100644 --- a/src/features/drawer/components/Section.jsx +++ b/src/features/drawer/components/Section.jsx @@ -28,7 +28,6 @@ const ADV_CATEGORIES = new Set([ 'pokestops', 'nests', 'stations', - 'tappables', ]) /** @param {{ category: keyof import('@rm/types').UIObject }} props */ diff --git a/src/features/drawer/components/SelectorList.jsx b/src/features/drawer/components/SelectorList.jsx index 393f341a0..a4f27a50d 100644 --- a/src/features/drawer/components/SelectorList.jsx +++ b/src/features/drawer/components/SelectorList.jsx @@ -56,6 +56,7 @@ function SelectorList({ category, subCategory, label, height = 400 }) { ) const easyMode = useStorage((s) => !!s.filters[category]?.easyMode) const search = useStorage((s) => s.searches[searchKey] || '') + const disableGutters = !['pokemon', 'tappables'].includes(category) const translated = React.useMemo( () => @@ -135,7 +136,7 @@ function SelectorList({ category, subCategory, label, height = 400 }) { return ( {translated.length > 10 && ( - + )} @@ -147,7 +148,7 @@ function SelectorList({ category, subCategory, label, height = 400 }) { /> )} {!!items.length && ( - + {t(search ? 'set_filtered' : 'set_all')} setAll('enable')}> From b729ac1df4566e3442e57e7af5f2428cecbdfd62 Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 29 Sep 2025 16:56:03 -0700 Subject: [PATCH 03/26] fix: icon --- src/services/Assets.js | 66 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/src/services/Assets.js b/src/services/Assets.js index d579d5a21..585c90d4c 100644 --- a/src/services/Assets.js +++ b/src/services/Assets.js @@ -2,6 +2,28 @@ /* eslint-disable no-console */ import { UICONS } from 'uicons.js' +/** + * Extract the first file extension from a set of filenames. + * @param {Set} set + */ +const extractExtension = (set) => { + if (!set) return undefined + const iterator = set.values() + let next = iterator.next() + while (!next.done) { + const entry = next.value + if (typeof entry === 'string' && entry.includes('.')) { + const parts = entry.split('.') + const ext = parts.pop() + if (ext) { + return ext + } + } + next = iterator.next() + } + return undefined +} + // /** // * // * @template {object} T @@ -144,6 +166,11 @@ export class UAssets { } } }) + if (Array.isArray(data.tappable)) { + this[name].tappable = new Set( + data.tappable.filter((iconName) => typeof iconName === 'string'), + ) + } } } catch (e) { console.error( @@ -418,12 +445,47 @@ export class UAssets { * @returns {string} */ getTappable(type = '0') { + const selection = this.selected.tappable + const pack = selection ? this[selection] : undefined try { - return this[this.selected.tappable]?.class?.tappable(type) + if (pack?.class && typeof pack.class.tappable === 'function') { + const iconPath = pack.class.tappable(type) + if (iconPath) { + return iconPath + } + } } catch (e) { console.error(`[${this.assetType.toUpperCase()}]`, e) - return `${this.fallback}/tappable/0.${this.fallbackExt}` } + + const normalized = (type || '0').toString() + const basePath = pack?.path || this.fallback + const tappableSet = + pack?.tappable instanceof Set ? pack.tappable : undefined + const extension = extractExtension(tappableSet) || this.fallbackExt + + const baseCandidates = [normalized, normalized.toLowerCase()] + if (normalized.startsWith('TAPPABLE_TYPE_')) { + const suffix = normalized.slice('TAPPABLE_TYPE_'.length) + baseCandidates.push(suffix, suffix.toLowerCase()) + } + const candidates = Array.from(new Set(baseCandidates)) + + if (tappableSet) { + for (let i = 0; i < candidates.length; i += 1) { + const candidate = candidates[i] + const filename = `${candidate}.${extension}` + if (tappableSet.has(filename)) { + return `${basePath}/tappable/${filename}` + } + } + const fallbackName = `0.${extension}` + if (tappableSet.has(fallbackName)) { + return `${basePath}/tappable/${fallbackName}` + } + } + + return `${basePath}/tappable/${normalized}.${extension}` } /** From addf8430c0170407d829531d690c4a9454d2a860 Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 29 Sep 2025 17:01:24 -0700 Subject: [PATCH 04/26] fix: in popup --- src/features/tappable/TappablePopup.jsx | 79 +++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/src/features/tappable/TappablePopup.jsx b/src/features/tappable/TappablePopup.jsx index 5cb74e571..ac181e567 100644 --- a/src/features/tappable/TappablePopup.jsx +++ b/src/features/tappable/TappablePopup.jsx @@ -3,6 +3,7 @@ import * as React from 'react' import Grid from '@mui/material/Unstable_Grid2' import Typography from '@mui/material/Typography' import Divider from '@mui/material/Divider' +import Tooltip from '@mui/material/Tooltip' import { useTranslation } from 'react-i18next' import { useStorage } from '@store/useStorage' @@ -10,6 +11,9 @@ import { useMemory } from '@store/useMemory' import { Navigation } from '@components/popups/Navigation' import { Coords } from '@components/popups/Coords' import { TimeStamp } from '@components/popups/TimeStamps' +import { StatusIcon } from '@components/StatusIcon' + +import { getTimeUntil } from '@utils/getTimeUntil' /** * @param {{ @@ -49,6 +53,8 @@ export function TappablePopup({ tappable, rewardIcon, iconSize }) { .join(' ') }, [tappable.type, t, i18n]) + const hasExpireTime = !!tappable.expire_timestamp + return ( )} - {tappable.expire_timestamp && ( - - - {t('disappear_time')} - + {hasExpireTime && ( + + )} {tappable.updated && ( - {t('last_updated')} + {t('last_seen')} )} ) } + +/** + * @param {{ + * expireTimestamp: number, + * verified: boolean, + * locale: string, + * t: import('i18next').TFunction + * }} props + */ +const TappableTimer = ({ expireTimestamp, verified, locale, t }) => { + const expireTimeMs = React.useMemo( + () => expireTimestamp * 1000, + [expireTimestamp], + ) + const [timer, setTimer] = React.useState(() => + getTimeUntil(expireTimeMs, true), + ) + + React.useEffect(() => { + setTimer(getTimeUntil(expireTimeMs, true)) + const interval = setInterval(() => { + setTimer(getTimeUntil(expireTimeMs, true)) + }, 1000) + return () => clearInterval(interval) + }, [expireTimeMs]) + + return ( + <> + + + {timer.str} + + + {new Date(expireTimeMs).toLocaleTimeString(locale || 'en', { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + })} + + + + + + + + + ) +} From 53c3630d119061577a01251fc39dda0de09162ea Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 11 Oct 2025 13:26:56 -0700 Subject: [PATCH 05/26] fix: tappable fallback --- src/services/Assets.js | 85 ++++++++++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 23 deletions(-) diff --git a/src/services/Assets.js b/src/services/Assets.js index 585c90d4c..cf306a7bc 100644 --- a/src/services/Assets.js +++ b/src/services/Assets.js @@ -444,34 +444,43 @@ export class UAssets { * @param {string} [type] * @returns {string} */ - getTappable(type = '0') { + getTappable(type = 'TAPPABLE_TYPE_POKEBALL') { const selection = this.selected.tappable const pack = selection ? this[selection] : undefined - try { - if (pack?.class && typeof pack.class.tappable === 'function') { - const iconPath = pack.class.tappable(type) - if (iconPath) { - return iconPath - } - } - } catch (e) { - console.error(`[${this.assetType.toUpperCase()}]`, e) - } - - const normalized = (type || '0').toString() const basePath = pack?.path || this.fallback const tappableSet = pack?.tappable instanceof Set ? pack.tappable : undefined const extension = extractExtension(tappableSet) || this.fallbackExt - const baseCandidates = [normalized, normalized.toLowerCase()] - if (normalized.startsWith('TAPPABLE_TYPE_')) { - const suffix = normalized.slice('TAPPABLE_TYPE_'.length) - baseCandidates.push(suffix, suffix.toLowerCase()) + const tryClass = (tappableType) => { + if (pack?.class && typeof pack.class.tappable === 'function') { + try { + const iconPath = pack.class.tappable(tappableType) + if (iconPath) { + return iconPath + } + } catch (e) { + console.error(`[${this.assetType.toUpperCase()}]`, e) + } + } + return undefined + } + + const buildCandidates = (tappableType) => { + const normalized = (tappableType || 'TAPPABLE_TYPE_POKEBALL').toString() + const baseCandidates = [normalized, normalized.toLowerCase()] + if (normalized.startsWith('TAPPABLE_TYPE_')) { + const suffix = normalized.slice('TAPPABLE_TYPE_'.length) + baseCandidates.push(suffix, suffix.toLowerCase()) + } + return Array.from(new Set(baseCandidates)) } - const candidates = Array.from(new Set(baseCandidates)) - if (tappableSet) { + const trySet = (tappableType) => { + if (!tappableSet) { + return undefined + } + const candidates = buildCandidates(tappableType) for (let i = 0; i < candidates.length; i += 1) { const candidate = candidates[i] const filename = `${candidate}.${extension}` @@ -479,13 +488,43 @@ export class UAssets { return `${basePath}/tappable/${filename}` } } - const fallbackName = `0.${extension}` - if (tappableSet.has(fallbackName)) { - return `${basePath}/tappable/${fallbackName}` + return undefined + } + + const buildDefaultPath = (tappableType) => { + const normalized = (tappableType || 'TAPPABLE_TYPE_POKEBALL').toString() + return `${basePath}/tappable/${normalized}.${extension}` + } + + const resolveType = (tappableType, allowDefault) => { + const fromClass = tryClass(tappableType) + if (fromClass) { + return { path: fromClass, found: true } + } + + const fromSet = trySet(tappableType) + if (fromSet) { + return { path: fromSet, found: true } + } + + if (allowDefault && !tappableSet) { + return { path: buildDefaultPath(tappableType), found: false } } + + return { path: undefined, found: false } + } + + const primary = resolveType(type, true) + if (primary.found) { + return primary.path + } + + const fallback = resolveType('TAPPABLE_TYPE_POKEBALL', false) + if (fallback.found) { + return fallback.path } - return `${basePath}/tappable/${normalized}.${extension}` + return this.getRewards(2, 1) } /** From 3eb5447d3f241be11ac24ac86daefffb571aed08 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 11 Oct 2025 14:28:17 -0700 Subject: [PATCH 06/26] fix: text --- src/features/tappable/TappablePopup.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/tappable/TappablePopup.jsx b/src/features/tappable/TappablePopup.jsx index ac181e567..24ae1c4eb 100644 --- a/src/features/tappable/TappablePopup.jsx +++ b/src/features/tappable/TappablePopup.jsx @@ -117,7 +117,7 @@ export function TappablePopup({ tappable, rewardIcon, iconSize }) { )} {tappable.updated && ( - {t('last_seen')} + {t('last_updated')} )} From e9c46e3d2514416ead71e141a338f13a2d259c6b Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 12 Oct 2025 18:10:57 -0700 Subject: [PATCH 07/26] fix: bubble --- src/assets/css/main.css | 31 ++-------- src/features/tappable/TappableTile.jsx | 78 +++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 33 deletions(-) diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 7d75d85bc..584f39c83 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -367,34 +367,13 @@ img { bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); - display: inline-flex; - align-items: center; - gap: 4px; - background-color: rgba(0, 0, 0, 0.85); - color: #fff; - padding: 2px 6px; - border-radius: 12px; - font-size: 12px; - line-height: 1; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); - white-space: nowrap; + z-index: 1; } -.tappable-marker__bubble::after { - content: ''; - position: absolute; - bottom: -6px; - left: 50%; - transform: translateX(-50%); - border-style: solid; - border-width: 6px 6px 0 6px; - border-color: rgba(0, 0, 0, 0.85) transparent transparent transparent; -} - -.tappable-marker__bubble img { - width: 20px; - height: 20px; - object-fit: contain; +.tappable-marker__bubble-svg { + display: block; + overflow: visible; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); } .quest-popup-img { diff --git a/src/features/tappable/TappableTile.jsx b/src/features/tappable/TappableTile.jsx index 718209163..b027a0c2d 100644 --- a/src/features/tappable/TappableTile.jsx +++ b/src/features/tappable/TappableTile.jsx @@ -3,6 +3,7 @@ import * as React from 'react' import { Marker, Popup } from 'react-leaflet' import { divIcon } from 'leaflet' +import { useTheme, alpha } from '@mui/material/styles' import { useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' @@ -35,6 +36,9 @@ const BaseTappableTile = (tappable) => { tappable.expire_timestamp ? getOpacity(tappable.expire_timestamp) : 1, [getOpacity, tappable.expire_timestamp], ) + const theme = useTheme() + const bubbleFill = alpha(theme.palette.background.paper, 0.5) + const bubbleTextColor = theme.palette.text.primary const { icon, rewardIcon, size } = React.useMemo(() => { if (!Icons || !tappable.item_id) { @@ -57,6 +61,65 @@ const BaseTappableTile = (tappable) => { tappableSize * -0.7 * (tappableMod?.offsetY || 1) + (tappableMod?.popupY || 0), ] + const count = tappable.count || 1 + const rewardSize = Icons.getSize('reward') + const paddingX = 10 + const paddingTop = 8 + const paddingBottom = 8 + const textHeight = count > 1 ? 14 : 0 + const bubbleWidth = rewardSize + paddingX * 2 + const bubbleHeight = paddingTop + rewardSize + textHeight + paddingBottom + const tailHeight = 16 + const tailWidth = Math.min(24, bubbleWidth * 0.45) + const svgHeight = bubbleHeight + tailHeight + const cornerRadius = 12 + const bubbleCenterX = bubbleWidth / 2 + const rewardX = paddingX + const rewardY = paddingTop + const countY = paddingTop + rewardSize + textHeight / 2 + const bubblePath = [ + `M${cornerRadius} 0`, + `H${bubbleWidth - cornerRadius}`, + `A${cornerRadius} ${cornerRadius} 0 0 1 ${bubbleWidth} ${cornerRadius}`, + `V${bubbleHeight - cornerRadius}`, + `A${cornerRadius} ${cornerRadius} 0 0 1 ${ + bubbleWidth - cornerRadius + } ${bubbleHeight}`, + `H${bubbleCenterX + tailWidth / 2}`, + `L${bubbleCenterX} ${bubbleHeight + tailHeight}`, + `L${bubbleCenterX - tailWidth / 2} ${bubbleHeight}`, + `H${cornerRadius}`, + `A${cornerRadius} ${cornerRadius} 0 0 1 0 ${bubbleHeight - cornerRadius}`, + `V${cornerRadius}`, + `A${cornerRadius} ${cornerRadius} 0 0 1 ${cornerRadius} 0`, + 'Z', + ].join(' ') + const countText = + count > 1 + ? `x${count}` + : '' + const bubbleSvg = ` + + ` const html = `
{ ? `
- ${tappable.item_id} - ${ - tappable.count && tappable.count > 1 - ? `x${tappable.count}` - : '' - } + ${bubbleSvg}
` : '' } @@ -104,6 +166,8 @@ const BaseTappableTile = (tappable) => { tappable.item_id, tappable.count, opacity, + bubbleFill, + bubbleTextColor, ]) if (!Icons || !icon) { From e0e24276d10837cfeaf5af6e419070aae39b3f86 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 12 Oct 2025 20:26:20 -0700 Subject: [PATCH 08/26] fix: center last updated --- src/components/popups/ExtraInfo.jsx | 6 +++--- src/components/popups/TimeStamps.jsx | 6 +++--- src/features/tappable/TappablePopup.jsx | 4 +++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/popups/ExtraInfo.jsx b/src/components/popups/ExtraInfo.jsx index a24a3bfd9..35dc7226b 100644 --- a/src/components/popups/ExtraInfo.jsx +++ b/src/components/popups/ExtraInfo.jsx @@ -6,14 +6,14 @@ import { useTranslation } from 'react-i18next' /** * - * @param {{ title?: string, data?: React.ReactNode, children?: React.ReactNode }} props + * @param {{ title?: string, data?: React.ReactNode, children?: React.ReactNode, xs?: number }} props * @returns */ -export const ExtraInfo = ({ title, data, children }) => { +export const ExtraInfo = ({ title, data, children, xs = 6 }) => { const { t } = useTranslation() return ( - + {title && ( {t(title)}: diff --git a/src/components/popups/TimeStamps.jsx b/src/components/popups/TimeStamps.jsx index 5550d26d2..28db7847b 100644 --- a/src/components/popups/TimeStamps.jsx +++ b/src/components/popups/TimeStamps.jsx @@ -8,10 +8,10 @@ import { ExtraInfo } from './ExtraInfo' /** * - * @param {{ time?: number, children: string }} props + * @param {{ time?: number, children: string, xs?: number }} props * @returns */ -export const TimeStamp = ({ time, children }) => { +export const TimeStamp = ({ time, children, xs = 6 }) => { const { i18n } = useTranslation() if (!time) return null @@ -23,7 +23,7 @@ export const TimeStamp = ({ time, children }) => { }) return ( - + diff --git a/src/features/tappable/TappablePopup.jsx b/src/features/tappable/TappablePopup.jsx index 24ae1c4eb..ac84512dc 100644 --- a/src/features/tappable/TappablePopup.jsx +++ b/src/features/tappable/TappablePopup.jsx @@ -117,7 +117,9 @@ export function TappablePopup({ tappable, rewardIcon, iconSize }) { )} {tappable.updated && ( - {t('last_updated')} + + {t('last_updated')} + )} From 4c55425248458c6234ba21e9487db588d6d16332 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 12 Oct 2025 20:33:32 -0700 Subject: [PATCH 09/26] chore: refine popup --- src/features/tappable/TappablePopup.jsx | 68 ++++++++++++++++++------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/src/features/tappable/TappablePopup.jsx b/src/features/tappable/TappablePopup.jsx index ac84512dc..3f4a3ed29 100644 --- a/src/features/tappable/TappablePopup.jsx +++ b/src/features/tappable/TappablePopup.jsx @@ -1,9 +1,12 @@ // @ts-check import * as React from 'react' +import ExpandMore from '@mui/icons-material/ExpandMore' import Grid from '@mui/material/Unstable_Grid2' import Typography from '@mui/material/Typography' import Divider from '@mui/material/Divider' import Tooltip from '@mui/material/Tooltip' +import IconButton from '@mui/material/IconButton' +import Collapse from '@mui/material/Collapse' import { useTranslation } from 'react-i18next' import { useStorage } from '@store/useStorage' @@ -27,6 +30,7 @@ export function TappablePopup({ tappable, rewardIcon, iconSize }) { const showCoords = useStorage( (s) => !!s.userSettings.tappables?.enableTappablePopupCoords, ) + const popups = useStorage((s) => s.popups) const masterfile = useMemory((s) => s.masterfile) const count = tappable.count ?? 1 @@ -54,6 +58,17 @@ export function TappablePopup({ tappable, rewardIcon, iconSize }) { }, [tappable.type, t, i18n]) const hasExpireTime = !!tappable.expire_timestamp + const hasExtras = showCoords || tappable.updated + + const handleExtrasToggle = React.useCallback(() => { + useStorage.setState((prev) => ({ + popups: { + ...prev.popups, + extras: !popups.extras, + pvp: false, + }, + })) + }, [popups.extras]) return ( )} - - - - {showCoords && ( - - - - )} - {(tappable.expire_timestamp || tappable.updated) && ( - - - - )} {hasExpireTime && ( )} - {tappable.updated && ( - - - {t('last_updated')} - + + + + {hasExtras && ( + + + + )} + {hasExtras && ( + + + + {tappable.updated && ( + + last_updated + + )} + {showCoords && ( + + + + )} + + + )} ) } From 99788ccd2ea4d1e58cea1c77fe165c19cf271830 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 12 Oct 2025 21:05:07 -0700 Subject: [PATCH 10/26] feat: additional features in popup --- src/features/tappable/TappablePopup.jsx | 142 ++++++++++++++++++++---- src/features/tappable/TappableTile.jsx | 7 +- 2 files changed, 127 insertions(+), 22 deletions(-) diff --git a/src/features/tappable/TappablePopup.jsx b/src/features/tappable/TappablePopup.jsx index 3f4a3ed29..7109b3f68 100644 --- a/src/features/tappable/TappablePopup.jsx +++ b/src/features/tappable/TappablePopup.jsx @@ -1,15 +1,18 @@ // @ts-check import * as React from 'react' import ExpandMore from '@mui/icons-material/ExpandMore' +import MoreVert from '@mui/icons-material/MoreVert' import Grid from '@mui/material/Unstable_Grid2' import Typography from '@mui/material/Typography' import Divider from '@mui/material/Divider' import Tooltip from '@mui/material/Tooltip' import IconButton from '@mui/material/IconButton' import Collapse from '@mui/material/Collapse' +import Menu from '@mui/material/Menu' +import MenuItem from '@mui/material/MenuItem' import { useTranslation } from 'react-i18next' -import { useStorage } from '@store/useStorage' +import { useStorage, setDeepStore } from '@store/useStorage' import { useMemory } from '@store/useMemory' import { Navigation } from '@components/popups/Navigation' import { Coords } from '@components/popups/Coords' @@ -31,6 +34,10 @@ export function TappablePopup({ tappable, rewardIcon, iconSize }) { (s) => !!s.userSettings.tappables?.enableTappablePopupCoords, ) const popups = useStorage((s) => s.popups) + const filterKey = tappable.item_id ? `q${tappable.item_id}` : '' + const filterEnabled = useStorage((s) => + filterKey ? s.filters?.tappables?.filter?.[filterKey]?.enabled : undefined, + ) const masterfile = useMemory((s) => s.masterfile) const count = tappable.count ?? 1 @@ -59,6 +66,9 @@ export function TappablePopup({ tappable, rewardIcon, iconSize }) { const hasExpireTime = !!tappable.expire_timestamp const hasExtras = showCoords || tappable.updated + const hasRewardIcon = !!rewardIcon + + const [menuAnchorEl, setMenuAnchorEl] = React.useState(null) const handleExtrasToggle = React.useCallback(() => { useStorage.setState((prev) => ({ @@ -70,6 +80,52 @@ export function TappablePopup({ tappable, rewardIcon, iconSize }) { })) }, [popups.extras]) + const handleMenuOpen = React.useCallback((event) => { + setMenuAnchorEl(event.currentTarget) + }, []) + + const handleMenuClose = React.useCallback(() => { + setMenuAnchorEl(null) + }, []) + + const handleHide = React.useCallback(() => { + setMenuAnchorEl(null) + if (tappable.id === undefined || tappable.id === null) return + useMemory.setState((prev) => ({ + hideList: new Set(prev.hideList).add(tappable.id), + })) + }, [tappable.id]) + + const handleExclude = React.useCallback(() => { + setMenuAnchorEl(null) + if (!filterKey) return + setDeepStore(`filters.tappables.filter.${filterKey}.enabled`, false) + }, [filterKey]) + + const handleTimer = React.useCallback(() => { + setMenuAnchorEl(null) + if (tappable.id === undefined || tappable.id === null) return + useMemory.setState((prev) => { + if (prev.timerList.includes(tappable.id)) { + return { + timerList: prev.timerList.filter((entry) => entry !== tappable.id), + } + } + return { timerList: [...prev.timerList, tappable.id] } + }) + }, [tappable.id]) + + const menuOptions = React.useMemo(() => { + const options = [ + { key: 'timer', label: 'timer', action: handleTimer }, + { key: 'hide', label: 'hide', action: handleHide }, + ] + if (filterKey && filterEnabled) { + options.push({ key: 'exclude', label: 'exclude', action: handleExclude }) + } + return options + }, [handleTimer, handleHide, handleExclude, filterKey, filterEnabled]) + return ( - {rewardIcon && ( - - {itemName} - - )} - - {itemName} - {count > 1 && ( - - ×{count} - + + {hasRewardIcon && ( + + {itemName} + )} - {formattedType && ( - - {formattedType} + + 14 ? 'subtitle1' : 'h6'}> + {itemName} - )} + {count > 1 && ( + + ×{count} + + )} + {formattedType && ( + + {formattedType} + + )} + + + + + + + + {menuOptions.map((option) => ( + + {t(option.label)} + + ))} + {hasExpireTime && ( { const Icons = useMemory((s) => s.Icons) const itemFilters = useStorage((s) => s.filters?.tappables?.filter || {}) - const showTimer = useStorage( + const showTimerSetting = useStorage( (s) => !!s.userSettings.tappables?.tappableTimers, ) + const timerForced = useMemory((s) => + tappable.id == null ? false : s.timerList.includes(tappable.id), + ) const [markerRef, setMarkerRef] = React.useState(null) useForcePopup(tappable.id, markerRef) @@ -193,7 +196,7 @@ const BaseTappableTile = (tappable) => { iconSize={size} /> - {showTimer && !!timers.length && ( + {(showTimerSetting || timerForced) && !!timers.length && ( )} From 6d3d27eafced0c3d8629bdec3ecbef9acb0601ba Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 12 Oct 2025 21:56:03 -0700 Subject: [PATCH 11/26] fix: tappable popup style matches pokestop --- src/features/tappable/TappablePopup.jsx | 79 ++++++++++++++++--------- src/features/tappable/TappableTile.jsx | 9 +-- 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/src/features/tappable/TappablePopup.jsx b/src/features/tappable/TappablePopup.jsx index 7109b3f68..0af1b4828 100644 --- a/src/features/tappable/TappablePopup.jsx +++ b/src/features/tappable/TappablePopup.jsx @@ -4,7 +4,6 @@ import ExpandMore from '@mui/icons-material/ExpandMore' import MoreVert from '@mui/icons-material/MoreVert' import Grid from '@mui/material/Unstable_Grid2' import Typography from '@mui/material/Typography' -import Divider from '@mui/material/Divider' import Tooltip from '@mui/material/Tooltip' import IconButton from '@mui/material/IconButton' import Collapse from '@mui/material/Collapse' @@ -18,6 +17,7 @@ import { Navigation } from '@components/popups/Navigation' import { Coords } from '@components/popups/Coords' import { TimeStamp } from '@components/popups/TimeStamps' import { StatusIcon } from '@components/StatusIcon' +import { Title } from '@components/popups/Title' import { getTimeUntil } from '@utils/getTimeUntil' @@ -25,10 +25,9 @@ import { getTimeUntil } from '@utils/getTimeUntil' * @param {{ * tappable: import('@rm/types').Tappable, * rewardIcon: string, - * iconSize: number, * }} props */ -export function TappablePopup({ tappable, rewardIcon, iconSize }) { +export function TappablePopup({ tappable, rewardIcon }) { const { t, i18n } = useTranslation() const showCoords = useStorage( (s) => !!s.userSettings.tappables?.enableTappablePopupCoords, @@ -39,6 +38,7 @@ export function TappablePopup({ tappable, rewardIcon, iconSize }) { filterKey ? s.filters?.tappables?.filter?.[filterKey]?.enabled : undefined, ) const masterfile = useMemory((s) => s.masterfile) + const Icons = useMemory((s) => s.Icons) const count = tappable.count ?? 1 const itemName = React.useMemo(() => { @@ -64,9 +64,18 @@ export function TappablePopup({ tappable, rewardIcon, iconSize }) { .join(' ') }, [tappable.type, t, i18n]) + const tappableIcon = React.useMemo(() => { + if (!Icons || typeof Icons.getTappable !== 'function') { + return '' + } + return Icons.getTappable(tappable.type) || '' + }, [Icons, tappable.type]) + const hasExpireTime = !!tappable.expire_timestamp const hasExtras = showCoords || tappable.updated const hasRewardIcon = !!rewardIcon + const hasTappableIcon = !!tappableIcon + const itemDisplayName = count > 1 ? `${itemName} x${count}` : itemName const [menuAnchorEl, setMenuAnchorEl] = React.useState(null) @@ -130,7 +139,7 @@ export function TappablePopup({ tappable, rewardIcon, iconSize }) { - {hasRewardIcon && ( + {hasTappableIcon ? ( {itemName} - )} - - 14 ? 'subtitle1' : 'h6'}> - {itemName} - - {count > 1 && ( - - ×{count} - - )} - {formattedType && ( - - {formattedType} - - )} - + ) : null} + {formattedType} + + ))} + + {hasRewardIcon ? ( + + {itemName} + + ) : null} + + {itemDisplayName} + + {hasExpireTime && ( - {tappable.updated && ( last_updated diff --git a/src/features/tappable/TappableTile.jsx b/src/features/tappable/TappableTile.jsx index e8cb24f95..a4b3b055b 100644 --- a/src/features/tappable/TappableTile.jsx +++ b/src/features/tappable/TappableTile.jsx @@ -43,7 +43,7 @@ const BaseTappableTile = (tappable) => { const bubbleFill = alpha(theme.palette.background.paper, 0.5) const bubbleTextColor = theme.palette.text.primary - const { icon, rewardIcon, size } = React.useMemo(() => { + const { icon, rewardIcon } = React.useMemo(() => { if (!Icons || !tappable.item_id) { return { icon: null, rewardIcon: '', size: 24 } } @@ -153,7 +153,6 @@ const BaseTappableTile = (tappable) => { ` return { - size: tappableSize, rewardIcon: tappableReward, icon: divIcon({ className: 'tappable-marker-icon', @@ -190,11 +189,7 @@ const BaseTappableTile = (tappable) => { eventHandlers={{ popupopen: handlePopupOpen }} > - + {(showTimerSetting || timerForced) && !!timers.length && ( From a51edae139dfdeb446eb98cde10980b2e085b26b Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 12 Oct 2025 22:02:32 -0700 Subject: [PATCH 12/26] chore: tappable translations --- packages/locales/lib/human/en.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index dca965d5b..0a5b9351c 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -806,6 +806,10 @@ "station": "Power Spot", "stations": "Power Spots", "tappables": "Tappables", + "tappable_type_breakfast": "Zygarde Cell", + "tappable_type_hat": "Party Hat", + "tappable_type_maple": "Apple", + "tappable_type_pokeball": "Poké Ball", "stations_filters": "Power Spots Filter Settings", "stations_options": "Power Spot Options", "all_stations": "All Power Spots", From 4bf623c20cd6946a22395cdcb525fcd230215494 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 12 Oct 2025 22:09:10 -0700 Subject: [PATCH 13/26] chore: resize tappable bubble --- src/features/tappable/TappableTile.jsx | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/features/tappable/TappableTile.jsx b/src/features/tappable/TappableTile.jsx index a4b3b055b..d1e2c3818 100644 --- a/src/features/tappable/TappableTile.jsx +++ b/src/features/tappable/TappableTile.jsx @@ -65,21 +65,24 @@ const BaseTappableTile = (tappable) => { (tappableMod?.popupY || 0), ] const count = tappable.count || 1 - const rewardSize = Icons.getSize('reward') - const paddingX = 10 - const paddingTop = 8 - const paddingBottom = 8 - const textHeight = count > 1 ? 14 : 0 - const bubbleWidth = rewardSize + paddingX * 2 + const hasCount = count > 1 + const defaultRewardSize = Icons.getSize('reward') + const rewardSize = hasCount ? defaultRewardSize : 25 + const textHeight = hasCount ? 14 : 0 + const paddingX = hasCount ? 10 : (30 - rewardSize) / 2 + const paddingTop = hasCount ? 8 : 4 + const paddingBottom = hasCount ? 8 : 4 + const bubbleWidth = hasCount ? rewardSize + paddingX * 2 : 30 const bubbleHeight = paddingTop + rewardSize + textHeight + paddingBottom - const tailHeight = 16 - const tailWidth = Math.min(24, bubbleWidth * 0.45) + const tailHeight = hasCount ? 16 : 12 + const tailWidth = Math.min(hasCount ? 24 : 16, bubbleWidth * 0.45) const svgHeight = bubbleHeight + tailHeight - const cornerRadius = 12 + const cornerRadius = hasCount ? 12 : 9 const bubbleCenterX = bubbleWidth / 2 const rewardX = paddingX const rewardY = paddingTop const countY = paddingTop + rewardSize + textHeight / 2 + const bubbleOffset = hasCount ? 6 : 4 const bubblePath = [ `M${cornerRadius} 0`, `H${bubbleWidth - cornerRadius}`, @@ -135,7 +138,9 @@ const BaseTappableTile = (tappable) => {
Date: Sun, 12 Oct 2025 22:11:49 -0700 Subject: [PATCH 14/26] chore: resize bubble --- src/features/tappable/TappableTile.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/tappable/TappableTile.jsx b/src/features/tappable/TappableTile.jsx index d1e2c3818..d8e9e9a7d 100644 --- a/src/features/tappable/TappableTile.jsx +++ b/src/features/tappable/TappableTile.jsx @@ -61,7 +61,8 @@ const BaseTappableTile = (tappable) => { const [tappableMod, rewardMod] = Icons.getModifiers('tappable', 'reward') const popupAnchor = [ tappableMod?.popupX || 0, - tappableSize * -0.7 * (tappableMod?.offsetY || 1) + + tappableSize * -0.7 * (tappableMod?.offsetY || 1) - + tappableSize / 2 + (tappableMod?.popupY || 0), ] const count = tappable.count || 1 @@ -161,7 +162,7 @@ const BaseTappableTile = (tappable) => { rewardIcon: tappableReward, icon: divIcon({ className: 'tappable-marker-icon', - iconAnchor: [tappableSize / 2, tappableSize / 2], + iconAnchor: [tappableSize / 2, tappableSize], popupAnchor, html, }), From d89b1b3a2b5a40ddf6965b424e440a4fb63fcf88 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 12 Oct 2025 23:01:42 -0700 Subject: [PATCH 15/26] feat: tappable rules to simplify apple and cell layout --- src/assets/css/main.css | 31 ++++++ src/features/tappable/TappablePopup.jsx | 123 +++++++++++++++--------- src/features/tappable/TappableTile.jsx | 48 +++++++-- src/features/tappable/displayRules.js | 61 ++++++++++++ 4 files changed, 210 insertions(+), 53 deletions(-) create mode 100644 src/features/tappable/displayRules.js diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 584f39c83..c87849231 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -362,6 +362,37 @@ img { object-fit: contain; } +.tappable-marker--reward-primary { + position: relative; + width: var(--tappable-size, 32px); + height: var(--tappable-size, 32px); + display: flex; + align-items: center; + justify-content: center; +} + +.tappable-marker__reward-primary { + width: 100%; + height: 100%; + object-fit: contain; +} + +.tappable-marker__count { + position: absolute; + right: -4px; + bottom: -6px; + min-width: 22px; + padding: 0 4px; + border-radius: 10px; + background: rgba(32, 32, 32, 0.8); + color: #fff; + font-size: 12px; + font-weight: 600; + line-height: 1.4; + text-align: center; + pointer-events: none; +} + .tappable-marker__bubble { position: absolute; bottom: calc(100% + 6px); diff --git a/src/features/tappable/TappablePopup.jsx b/src/features/tappable/TappablePopup.jsx index 0af1b4828..8ca3d30b6 100644 --- a/src/features/tappable/TappablePopup.jsx +++ b/src/features/tappable/TappablePopup.jsx @@ -20,6 +20,7 @@ import { StatusIcon } from '@components/StatusIcon' import { Title } from '@components/popups/Title' import { getTimeUntil } from '@utils/getTimeUntil' +import { getTappableDisplaySettings } from './displayRules' /** * @param {{ @@ -39,6 +40,7 @@ export function TappablePopup({ tappable, rewardIcon }) { ) const masterfile = useMemory((s) => s.masterfile) const Icons = useMemory((s) => s.Icons) + const displaySettings = getTappableDisplaySettings(tappable) const count = tappable.count ?? 1 const itemName = React.useMemo(() => { @@ -74,7 +76,9 @@ export function TappablePopup({ tappable, rewardIcon }) { const hasExpireTime = !!tappable.expire_timestamp const hasExtras = showCoords || tappable.updated const hasRewardIcon = !!rewardIcon - const hasTappableIcon = !!tappableIcon + const useRewardAsPrimary = + displaySettings.popup.rewardAsPrimary && hasRewardIcon + const hasTappableIcon = !!tappableIcon && !useRewardAsPrimary const itemDisplayName = count > 1 ? `${itemName} x${count}` : itemName const [menuAnchorEl, setMenuAnchorEl] = React.useState(null) @@ -151,28 +155,57 @@ export function TappablePopup({ tappable, rewardIcon }) { justifyContent="center" spacing={1} > - {hasTappableIcon ? ( - - {formattedType} - - ) : null} - - {formattedType} - + {useRewardAsPrimary ? ( + <> + {hasRewardIcon ? ( + + {itemName} + + ) : null} + + {itemDisplayName} + + + ) : ( + <> + {hasTappableIcon ? ( + + {formattedType} + + ) : null} + + {formattedType} + + + )} ))} - - {hasRewardIcon ? ( - - {itemName} + {!useRewardAsPrimary && ( + + {hasRewardIcon ? ( + + {itemName} + + ) : null} + + {itemDisplayName} - ) : null} - - {itemDisplayName} - + )} {hasExpireTime && ( { const theme = useTheme() const bubbleFill = alpha(theme.palette.background.paper, 0.5) const bubbleTextColor = theme.palette.text.primary + const displaySettings = getTappableDisplaySettings(tappable) + const useRewardPrimary = displaySettings.map.rewardAsPrimary const { icon, rewardIcon } = React.useMemo(() => { if (!Icons || !tappable.item_id) { @@ -50,14 +53,6 @@ const BaseTappableTile = (tappable) => { const filterKey = `q${tappable.item_id}` const tappableSize = Icons.getSize('tappable', itemFilters[filterKey]?.size) const tappableIcon = Icons.getTappable(tappable.type) - const tappableReward = Icons.getRewards( - 2, - tappable.item_id, - tappable.count || 1, - ) - if (!tappableIcon) { - return { icon: null, rewardIcon: '', size: tappableSize } - } const [tappableMod, rewardMod] = Icons.getModifiers('tappable', 'reward') const popupAnchor = [ tappableMod?.popupX || 0, @@ -65,9 +60,43 @@ const BaseTappableTile = (tappable) => { tappableSize / 2 + (tappableMod?.popupY || 0), ] + const tappableReward = Icons.getRewards( + 2, + tappable.item_id, + tappable.count || 1, + ) const count = tappable.count || 1 - const hasCount = count > 1 + if (useRewardPrimary && tappableReward) { + const countBadge = + count > 1 ? `x${count}` : '' + const html = ` +
+ ${tappable.type || ''} + ${countBadge} +
+ ` + return { + rewardIcon: tappableReward, + icon: divIcon({ + className: 'tappable-marker-icon', + iconAnchor: [tappableSize / 2, tappableSize], + popupAnchor, + html, + }), + } + } + if (!tappableIcon) { + return { icon: null, rewardIcon: '', size: tappableSize } + } const defaultRewardSize = Icons.getSize('reward') + const hasCount = count > 1 const rewardSize = hasCount ? defaultRewardSize : 25 const textHeight = hasCount ? 14 : 0 const paddingX = hasCount ? 10 : (30 - rewardSize) / 2 @@ -176,6 +205,7 @@ const BaseTappableTile = (tappable) => { opacity, bubbleFill, bubbleTextColor, + useRewardPrimary, ]) if (!Icons || !icon) { diff --git a/src/features/tappable/displayRules.js b/src/features/tappable/displayRules.js new file mode 100644 index 000000000..fa8c2179b --- /dev/null +++ b/src/features/tappable/displayRules.js @@ -0,0 +1,61 @@ +// @ts-check + +const rewardPrimaryBehavior = Object.freeze({ + map: Object.freeze({ + rewardAsPrimary: true, + }), + popup: Object.freeze({ + rewardAsPrimary: true, + }), +}) + +const defaultBehavior = Object.freeze({ + map: Object.freeze({ + rewardAsPrimary: false, + }), + popup: Object.freeze({ + rewardAsPrimary: false, + }), +}) + +const toFrozenSet = (values) => Object.freeze(new Set(values.map(Number))) + +const rewardPrimaryRules = [ + { + types: Object.freeze(new Set(['TAPPABLE_TYPE_MAPLE'])), + itemIds: toFrozenSet([1151, 1152, 1155]), + behavior: rewardPrimaryBehavior, + }, + { + types: Object.freeze(new Set(['TAPPABLE_TYPE_BREAKFAST'])), + itemIds: toFrozenSet([650]), + behavior: rewardPrimaryBehavior, + }, +] + +/** + * Determines display overrides for a tappable marker and popup. + * @param {import('@rm/types').Tappable} tappable + */ +export function getTappableDisplaySettings(tappable) { + const typeKey = tappable?.type?.toString() + if (!typeKey) { + return defaultBehavior + } + + const itemId = Number(tappable?.item_id) + if (!Number.isFinite(itemId)) { + return defaultBehavior + } + + for (let i = 0; i < rewardPrimaryRules.length; i += 1) { + const rule = rewardPrimaryRules[i] + if (rule.types.has(typeKey) && rule.itemIds.has(itemId)) { + return rule.behavior + } + } + + return defaultBehavior +} + +export const TAPPABLE_DISPLAY_DEFAULT = defaultBehavior From 991eb669c50579cd0708a237391fe59b6d45b556 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 12 Oct 2025 23:22:00 -0700 Subject: [PATCH 16/26] fix: remove unnecessary stuff --- config/default.json | 1 - server/src/models/Tappable.js | 12 ++---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/config/default.json b/config/default.json index 5ebf07548..064ae6553 100644 --- a/config/default.json +++ b/config/default.json @@ -120,7 +120,6 @@ "portalUpdateLimit": 30, "weatherCellLimit": 3, "stationUpdateLimit": 9, - "tappableUpdateLimit": 6, "stationInactiveLimitDays": 120, "searchResultsLimit": 15, "searchSoftKmLimit": 10, diff --git a/server/src/models/Tappable.js b/server/src/models/Tappable.js index 09f4e4352..f96f54f73 100644 --- a/server/src/models/Tappable.js +++ b/server/src/models/Tappable.js @@ -28,7 +28,7 @@ class Tappable extends Model { const { filters: filterArgs = {}, minLat, maxLat, minLon, maxLon } = args - const { queryLimits = {}, tappableUpdateLimit = 6 } = config.getSafe('api') + const { queryLimits = {} } = config.getSafe('api') const timestamp = getEpoch() const query = this.query().select([ @@ -58,15 +58,7 @@ class Tappable extends Model { query.whereNull('pokemon_id').whereNotNull('item_id') - query.andWhere((builder) => { - builder - .whereNull('expire_timestamp') - .orWhere('expire_timestamp', '>', timestamp) - }) - - if (tappableUpdateLimit > 0) { - query.andWhere('updated', '>', timestamp - tappableUpdateLimit * 60 * 60) - } + query.andWhere('expire_timestamp', '>', timestamp) const itemIds = [] Object.keys(filterArgs).forEach((key) => { From 56dbd36c4929d9baebdd48c5bf61c670b5ae37ab Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 12 Oct 2025 23:36:31 -0700 Subject: [PATCH 17/26] fix: interaction range --- config/default.json | 1 - server/src/ui/clientOptions.js | 7 ----- src/features/tappable/TappableTile.jsx | 38 ++++++++++++++++++++------ 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/config/default.json b/config/default.json index 064ae6553..2f0573a76 100644 --- a/config/default.json +++ b/config/default.json @@ -402,7 +402,6 @@ "clustering": true, "tappableTimers": false, "interactionRanges": false, - "customRange": 0, "tappablesOpacity": true, "enableTappablePopupCoords": false, "opacityTenMinutes": 0.75, diff --git a/server/src/ui/clientOptions.js b/server/src/ui/clientOptions.js index 868922823..317937269 100644 --- a/server/src/ui/clientOptions.js +++ b/server/src/ui/clientOptions.js @@ -187,13 +187,6 @@ function clientOptions(perms) { perm: ['tappables'], category: 'markers', }, - customRange: { - type: 'number', - perm: ['tappables'], - min: 0, - max: 5000, - category: 'markers', - }, tappablesOpacity: { type: 'bool', perm: ['tappables'], diff --git a/src/features/tappable/TappableTile.jsx b/src/features/tappable/TappableTile.jsx index d5965e9d0..3758cf489 100644 --- a/src/features/tappable/TappableTile.jsx +++ b/src/features/tappable/TappableTile.jsx @@ -1,11 +1,11 @@ /* eslint-disable react/destructuring-assignment */ // @ts-check import * as React from 'react' -import { Marker, Popup } from 'react-leaflet' +import { Marker, Popup, Circle } from 'react-leaflet' import { divIcon } from 'leaflet' import { useTheme, alpha } from '@mui/material/styles' -import { useMemory } from '@store/useMemory' +import { useMemory, basicEqualFn } from '@store/useMemory' import { useStorage } from '@store/useStorage' import { useManualPopupTracker } from '@hooks/useManualPopupTracker' import { useForcePopup } from '@hooks/useForcePopup' @@ -22,12 +22,27 @@ import { getTappableDisplaySettings } from './displayRules' const BaseTappableTile = (tappable) => { const Icons = useMemory((s) => s.Icons) const itemFilters = useStorage((s) => s.filters?.tappables?.filter || {}) - const showTimerSetting = useStorage( - (s) => !!s.userSettings.tappables?.tappableTimers, - ) - const timerForced = useMemory((s) => - tappable.id == null ? false : s.timerList.includes(tappable.id), - ) + const [timerForced, interactionRangeZoom] = useMemory((s) => { + const { + timerList, + config: { general = {} }, + } = s + const zoomLimit = Number.isFinite(general.interactionRangeZoom) + ? general.interactionRangeZoom + : 15 + return [ + tappable.id == null ? false : timerList.includes(tappable.id), + zoomLimit, + ] + }, basicEqualFn) + const [showTimerSetting, showInteractionRange] = useStorage((s) => { + const { userSettings, zoom } = s + return [ + !!userSettings.tappables?.tappableTimers, + !!userSettings.tappables?.interactionRanges && + zoom >= interactionRangeZoom, + ] + }, basicEqualFn) const [markerRef, setMarkerRef] = React.useState(null) useForcePopup(tappable.id, markerRef) @@ -230,6 +245,13 @@ const BaseTappableTile = (tappable) => { {(showTimerSetting || timerForced) && !!timers.length && ( )} + {showInteractionRange && ( + + )} ) } From 7572ef2c93f7a739844e44634413f17431f017a4 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 12 Oct 2025 23:39:45 -0700 Subject: [PATCH 18/26] feat: spacial rend range for tappables --- config/default.json | 1 + server/src/ui/clientOptions.js | 5 +++++ src/features/tappable/TappableTile.jsx | 30 +++++++++++++++++++------- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/config/default.json b/config/default.json index 2f0573a76..faa216988 100644 --- a/config/default.json +++ b/config/default.json @@ -402,6 +402,7 @@ "clustering": true, "tappableTimers": false, "interactionRanges": false, + "spacialRendRange": false, "tappablesOpacity": true, "enableTappablePopupCoords": false, "opacityTenMinutes": 0.75, diff --git a/server/src/ui/clientOptions.js b/server/src/ui/clientOptions.js index 317937269..cf866c5a3 100644 --- a/server/src/ui/clientOptions.js +++ b/server/src/ui/clientOptions.js @@ -187,6 +187,11 @@ function clientOptions(perms) { perm: ['tappables'], category: 'markers', }, + spacialRendRange: { + type: 'bool', + perm: ['tappables'], + category: 'markers', + }, tappablesOpacity: { type: 'bool', perm: ['tappables'], diff --git a/src/features/tappable/TappableTile.jsx b/src/features/tappable/TappableTile.jsx index 3758cf489..d51d5dcc8 100644 --- a/src/features/tappable/TappableTile.jsx +++ b/src/features/tappable/TappableTile.jsx @@ -35,14 +35,17 @@ const BaseTappableTile = (tappable) => { zoomLimit, ] }, basicEqualFn) - const [showTimerSetting, showInteractionRange] = useStorage((s) => { - const { userSettings, zoom } = s - return [ - !!userSettings.tappables?.tappableTimers, - !!userSettings.tappables?.interactionRanges && - zoom >= interactionRangeZoom, - ] - }, basicEqualFn) + const [showTimerSetting, showInteractionRange, showSpacialRendRange] = + useStorage((s) => { + const { userSettings, zoom } = s + return [ + !!userSettings.tappables?.tappableTimers, + !!userSettings.tappables?.interactionRanges && + zoom >= interactionRangeZoom, + !!userSettings.tappables?.spacialRendRange && + zoom >= interactionRangeZoom, + ] + }, basicEqualFn) const [markerRef, setMarkerRef] = React.useState(null) useForcePopup(tappable.id, markerRef) @@ -252,6 +255,17 @@ const BaseTappableTile = (tappable) => { pathOptions={{ color: '#0DA8E7', weight: 1 }} /> )} + {showSpacialRendRange && ( + + )} ) } From dbe3a35b65470a1bb5bc210d0a90cab17339b827 Mon Sep 17 00:00:00 2001 From: Mygod Date: Mon, 13 Oct 2025 00:13:54 -0700 Subject: [PATCH 19/26] fix: code review --- src/store/useMemory.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/store/useMemory.js b/src/store/useMemory.js index 04faead03..ccd3d732c 100644 --- a/src/store/useMemory.js +++ b/src/store/useMemory.js @@ -59,7 +59,6 @@ import { create } from 'zustand' * nests: string[], * stations: string[], * tappables: string[], - * tappables: string[], * questConditions: Record, * } * manualParams: { From d7197d3c0e60252f031a38bc3c7da0b285a4515a Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 14 Oct 2025 19:55:31 -0700 Subject: [PATCH 20/26] chore: uicons.js for tappable --- package.json | 2 +- src/services/Assets.js | 110 +++-------------------------------------- yarn.lock | 8 +-- 3 files changed, 13 insertions(+), 107 deletions(-) diff --git a/package.json b/package.json index 3f9490df4..ca2c82129 100644 --- a/package.json +++ b/package.json @@ -174,7 +174,7 @@ "source-map": "^0.7.4", "suncalc": "^1.9.0", "supercluster": "^8.0.1", - "uicons.js": "2.0.3", + "uicons.js": "2.1.0", "zustand": "4.4.6" }, "devDependencies": { diff --git a/src/services/Assets.js b/src/services/Assets.js index cf306a7bc..e0439db6c 100644 --- a/src/services/Assets.js +++ b/src/services/Assets.js @@ -2,28 +2,6 @@ /* eslint-disable no-console */ import { UICONS } from 'uicons.js' -/** - * Extract the first file extension from a set of filenames. - * @param {Set} set - */ -const extractExtension = (set) => { - if (!set) return undefined - const iterator = set.values() - let next = iterator.next() - while (!next.done) { - const entry = next.value - if (typeof entry === 'string' && entry.includes('.')) { - const parts = entry.split('.') - const ext = parts.pop() - if (ext) { - return ext - } - } - next = iterator.next() - } - return undefined -} - // /** // * // * @template {object} T @@ -166,11 +144,6 @@ export class UAssets { } } }) - if (Array.isArray(data.tappable)) { - this[name].tappable = new Set( - data.tappable.filter((iconName) => typeof iconName === 'string'), - ) - } } } catch (e) { console.error( @@ -445,85 +418,18 @@ export class UAssets { * @returns {string} */ getTappable(type = 'TAPPABLE_TYPE_POKEBALL') { - const selection = this.selected.tappable - const pack = selection ? this[selection] : undefined - const basePath = pack?.path || this.fallback - const tappableSet = - pack?.tappable instanceof Set ? pack.tappable : undefined - const extension = extractExtension(tappableSet) || this.fallbackExt - - const tryClass = (tappableType) => { + try { + const selection = this.selected.tappable + const pack = selection ? this[selection] : undefined if (pack?.class && typeof pack.class.tappable === 'function') { - try { - const iconPath = pack.class.tappable(tappableType) - if (iconPath) { - return iconPath - } - } catch (e) { - console.error(`[${this.assetType.toUpperCase()}]`, e) - } - } - return undefined - } - - const buildCandidates = (tappableType) => { - const normalized = (tappableType || 'TAPPABLE_TYPE_POKEBALL').toString() - const baseCandidates = [normalized, normalized.toLowerCase()] - if (normalized.startsWith('TAPPABLE_TYPE_')) { - const suffix = normalized.slice('TAPPABLE_TYPE_'.length) - baseCandidates.push(suffix, suffix.toLowerCase()) - } - return Array.from(new Set(baseCandidates)) - } - - const trySet = (tappableType) => { - if (!tappableSet) { - return undefined - } - const candidates = buildCandidates(tappableType) - for (let i = 0; i < candidates.length; i += 1) { - const candidate = candidates[i] - const filename = `${candidate}.${extension}` - if (tappableSet.has(filename)) { - return `${basePath}/tappable/${filename}` + const iconPath = pack.class.tappable(type) + if (iconPath) { + return iconPath } } - return undefined - } - - const buildDefaultPath = (tappableType) => { - const normalized = (tappableType || 'TAPPABLE_TYPE_POKEBALL').toString() - return `${basePath}/tappable/${normalized}.${extension}` - } - - const resolveType = (tappableType, allowDefault) => { - const fromClass = tryClass(tappableType) - if (fromClass) { - return { path: fromClass, found: true } - } - - const fromSet = trySet(tappableType) - if (fromSet) { - return { path: fromSet, found: true } - } - - if (allowDefault && !tappableSet) { - return { path: buildDefaultPath(tappableType), found: false } - } - - return { path: undefined, found: false } - } - - const primary = resolveType(type, true) - if (primary.found) { - return primary.path - } - - const fallback = resolveType('TAPPABLE_TYPE_POKEBALL', false) - if (fallback.found) { - return fallback.path + } catch (e) { + console.error(`[${this.assetType.toUpperCase()}]`, e) } - return this.getRewards(2, 1) } diff --git a/yarn.lock b/yarn.lock index ca946632e..3059c9ad6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9820,10 +9820,10 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== -uicons.js@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/uicons.js/-/uicons.js-2.0.3.tgz#38833f9cfad5fcbf6968ff3f0a1f36ab47dd40be" - integrity sha512-+hFdKLHkB1bIrwNNnDO8HBe0R10UuJDZ6xqkroigBqouHxZExuxznY2GlDUqGXv7RHB9OFNzFLo5mFzeudlf7w== +uicons.js@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uicons.js/-/uicons.js-2.1.0.tgz#9aaa6254aeba25dad5d4672d2fb840ec4c4c97b0" + integrity sha512-Pmg2QBvQuy3VaTzZDKDCM0/K+fgKf0EK/ZGOnxzkN33HiRCtMOxcSmN80/tO1WFDzCXG0FvbF/zj2rgPDy5zTA== uid-safe@~2.1.5: version "2.1.5" From 88a882ac74daaccb961fb23d0bbd6388aa05e361 Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 14 Oct 2025 20:54:29 -0700 Subject: [PATCH 21/26] fix: slop --- src/features/tappable/TappablePopup.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/tappable/TappablePopup.jsx b/src/features/tappable/TappablePopup.jsx index 8ca3d30b6..439c635c2 100644 --- a/src/features/tappable/TappablePopup.jsx +++ b/src/features/tappable/TappablePopup.jsx @@ -88,7 +88,6 @@ export function TappablePopup({ tappable, rewardIcon }) { popups: { ...prev.popups, extras: !popups.extras, - pvp: false, }, })) }, [popups.extras]) From 49c108213721d7aea6cf8a613b99c77094141d19 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 15 Oct 2025 00:12:51 -0700 Subject: [PATCH 22/26] feat: advanced tappable settings --- server/src/ui/advMenus.js | 8 +++- src/components/filters/FilterMenu.jsx | 15 +++---- src/features/drawer/components/Section.jsx | 1 + src/hooks/useFilter.js | 23 +++++------ src/pages/map/components/Effects.jsx | 2 + src/pages/map/hooks/useGenTappables.js | 46 ++++++++++++++++++++++ 6 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 src/pages/map/hooks/useGenTappables.js diff --git a/server/src/ui/advMenus.js b/server/src/ui/advMenus.js index 0487e8cb7..4b2504353 100644 --- a/server/src/ui/advMenus.js +++ b/server/src/ui/advMenus.js @@ -20,7 +20,7 @@ const CATEGORIES = /** @type {const} */ ({ stations: ['pokemon'], pokemon: ['pokemon'], nests: ['pokemon'], - tappables: ['items'], + tappables: ['tappables'], }) /** @@ -117,6 +117,12 @@ function advMenus(perms) { categories: Object.fromEntries( CATEGORIES.tappables.map((item) => [item, false]), ), + others: { + reverse: false, + selected: false, + unselected: false, + onlyAvailable: true, + }, }, }, pokemon: { diff --git a/src/components/filters/FilterMenu.jsx b/src/components/filters/FilterMenu.jsx index 2557fb036..6d204a730 100644 --- a/src/components/filters/FilterMenu.jsx +++ b/src/components/filters/FilterMenu.jsx @@ -2,6 +2,7 @@ import * as React from 'react' import { toggleDialog, useLayoutStore } from '@store/useLayoutStore' +import { useMemory } from '@store/useMemory' import { StandardItem } from '@components/virtual/StandardItem' import { Menu } from '@components/Menu' import { Header } from '@components/dialogs/Header' @@ -19,13 +20,13 @@ const EXTRA_BUTTONS = [ export function FilterMenu() { const { open, category, type } = useLayoutStore((s) => s.dialog) + const menuConfig = useMemory((s) => (category ? s.menus?.[category] : null)) - return (category === 'pokemon' || - category === 'gyms' || - category === 'pokestops' || - category === 'nests' || - category === 'stations') && - type === 'filters' ? ( + if (!category || !menuConfig || type !== 'filters') { + return null + } + + return ( } - ) : null + ) } diff --git a/src/features/drawer/components/Section.jsx b/src/features/drawer/components/Section.jsx index e78274d48..749cd236e 100644 --- a/src/features/drawer/components/Section.jsx +++ b/src/features/drawer/components/Section.jsx @@ -28,6 +28,7 @@ const ADV_CATEGORIES = new Set([ 'pokestops', 'nests', 'stations', + 'tappables', ]) /** @param {{ category: keyof import('@rm/types').UIObject }} props */ diff --git a/src/hooks/useFilter.js b/src/hooks/useFilter.js index 8e37daa9a..8b6acce3f 100644 --- a/src/hooks/useFilter.js +++ b/src/hooks/useFilter.js @@ -24,8 +24,8 @@ const filteringPokemon = [ export function useFilter(category, webhookCategory, reqCategories) { const { t } = useTranslation() const tempFilters = webhookCategory - ? useWebhookStore((s) => s.tempFilters) - : useStorage((s) => s.filters[category].filter) + ? useWebhookStore((s) => s.tempFilters || {}) + : useStorage((s) => s.filters?.[category]?.filter || {}) const search = useGetDeepStore(`searches.${category}Advanced`, '') .toLowerCase() @@ -37,15 +37,15 @@ export function useFilter(category, webhookCategory, reqCategories) { menuFilters, menus: { [category]: staticMenus }, } = useMemory.getState() - const menus = useStorage((s) => s.menus[category].filters) + const menus = useStorage((s) => s.menus?.[category]?.filters || {}) const { - generations, - types, - rarity, - historicRarity, - forms, - others, - categories, + generations = {}, + types = {}, + rarity = {}, + historicRarity = {}, + forms = {}, + others = { onlyAvailable: true }, + categories = {}, } = menus const tempAdvFilter = {} @@ -55,7 +55,8 @@ export function useFilter(category, webhookCategory, reqCategories) { let switchKey Object.keys(menus).forEach((subCategory) => { - tempAdvFilter[subCategory] = Object.values(menus[subCategory]).every( + const options = menus[subCategory] || {} + tempAdvFilter[subCategory] = Object.values(options).every( (val) => val === false, ) }) diff --git a/src/pages/map/components/Effects.jsx b/src/pages/map/components/Effects.jsx index 7541ce110..16f08294d 100644 --- a/src/pages/map/components/Effects.jsx +++ b/src/pages/map/components/Effects.jsx @@ -12,6 +12,7 @@ import { useMemory } from '@store/useMemory' import { useGenGyms } from '../hooks/useGenGyms' import { useGenPokestops } from '../hooks/useGenPokestops' import { useGenPokemon } from '../hooks/useGenPokemon' +import { useGenTappables } from '../hooks/useGenTappables' export function Effects() { const params = useParams() @@ -22,6 +23,7 @@ export function Effects() { useGenGyms() useGenPokestops() useGenPokemon() + useGenTappables() const isMobile = useMediaQuery( (/** @type {import('@mui/system').Theme} */ theme) => diff --git a/src/pages/map/hooks/useGenTappables.js b/src/pages/map/hooks/useGenTappables.js new file mode 100644 index 000000000..27e57ddca --- /dev/null +++ b/src/pages/map/hooks/useGenTappables.js @@ -0,0 +1,46 @@ +// @ts-check +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' + +import { useMemory } from '@store/useMemory' + +export function useGenTappables() { + const { t } = useTranslation() + const tappables = useMemory((s) => s.filters.tappables) + const categories = useMemory((s) => s.menus.tappables?.categories || []) + + useEffect(() => { + if (!tappables?.filter || !categories.includes('tappables')) { + return + } + + /** @type {import('@rm/types').ClientFilterObj['tappables']} */ + const tappableFilters = {} + + Object.keys(tappables.filter).forEach((id) => { + if (id === 'global' || id === 's0') return + const itemId = id.startsWith('q') ? id.slice(1) : id + const name = t(`item_${itemId}`, `#${itemId}`) + tappableFilters[id] = { + name, + perms: ['tappables'], + } + tappableFilters[id].searchMeta = + `${t('tappables').toLowerCase()} ${String(name).toLowerCase()}` + }) + + if (Object.keys(tappableFilters).length === 0) { + return + } + + useMemory.setState((prev) => ({ + menuFilters: { + ...prev.menuFilters, + tappables: { + ...(prev.menuFilters?.tappables || {}), + ...tappableFilters, + }, + }, + })) + }, [tappables, categories, t]) +} From 58fbc4ee508320fcb8025eb0444566a020626899 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 15 Oct 2025 00:18:55 -0700 Subject: [PATCH 23/26] fix: s0 confusing --- server/src/filters/builder/tappable.js | 2 +- server/src/models/Tappable.js | 2 +- src/features/drawer/components/SelectorList.jsx | 2 +- src/pages/map/hooks/useGenTappables.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/filters/builder/tappable.js b/server/src/filters/builder/tappable.js index 554845f62..be7780d90 100644 --- a/server/src/filters/builder/tappable.js +++ b/server/src/filters/builder/tappable.js @@ -8,7 +8,7 @@ const { BaseFilter } = require('../Base') * @returns {Record} */ function buildTappables(perms, defaults) { - const filters = { s0: new BaseFilter() } + const filters = { q0: new BaseFilter() } if (!perms.tappables) { return filters } diff --git a/server/src/models/Tappable.js b/server/src/models/Tappable.js index f96f54f73..cd1f8e7ea 100644 --- a/server/src/models/Tappable.js +++ b/server/src/models/Tappable.js @@ -66,7 +66,7 @@ class Tappable extends Model { switch (key.charAt(0)) { case 'q': { const itemId = Number.parseInt(key.slice(1), 10) - if (!Number.isNaN(itemId)) { + if (!Number.isNaN(itemId) && itemId !== 0) { itemIds.push(itemId) } break diff --git a/src/features/drawer/components/SelectorList.jsx b/src/features/drawer/components/SelectorList.jsx index a4f27a50d..1787ab510 100644 --- a/src/features/drawer/components/SelectorList.jsx +++ b/src/features/drawer/components/SelectorList.jsx @@ -94,7 +94,7 @@ function SelectorList({ category, subCategory, label, height = 400 }) { case 'gyms': return key.startsWith('t') case 'tappables': - return key.startsWith('q') + return key.startsWith('q') && key !== 'q0' default: return Number.isInteger(Number(key.charAt(0))) } diff --git a/src/pages/map/hooks/useGenTappables.js b/src/pages/map/hooks/useGenTappables.js index 27e57ddca..56b655cd6 100644 --- a/src/pages/map/hooks/useGenTappables.js +++ b/src/pages/map/hooks/useGenTappables.js @@ -18,7 +18,7 @@ export function useGenTappables() { const tappableFilters = {} Object.keys(tappables.filter).forEach((id) => { - if (id === 'global' || id === 's0') return + if (id === 'global' || id === 'q0') return const itemId = id.startsWith('q') ? id.slice(1) : id const name = t(`item_${itemId}`, `#${itemId}`) tappableFilters[id] = { From 52fcf7e31dd66a3e19118b7784627a1394f224c6 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 15 Oct 2025 00:34:16 -0700 Subject: [PATCH 24/26] fix: something? --- src/pages/map/hooks/useGenTappables.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/map/hooks/useGenTappables.js b/src/pages/map/hooks/useGenTappables.js index 56b655cd6..ceb6fbe33 100644 --- a/src/pages/map/hooks/useGenTappables.js +++ b/src/pages/map/hooks/useGenTappables.js @@ -7,10 +7,13 @@ import { useMemory } from '@store/useMemory' export function useGenTappables() { const { t } = useTranslation() const tappables = useMemory((s) => s.filters.tappables) - const categories = useMemory((s) => s.menus.tappables?.categories || []) + const categories = useMemory((s) => s.menus.tappables?.categories) useEffect(() => { - if (!tappables?.filter || !categories.includes('tappables')) { + const hasCategory = Array.isArray(categories) + ? categories.includes('tappables') + : !!categories?.tappables + if (!tappables?.filter || !hasCategory) { return } From 69978e9da8c53cada3ad1f51a05d923310a6ef5c Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 15 Oct 2025 09:34:25 -0700 Subject: [PATCH 25/26] fix: slop --- server/src/models/Tappable.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/models/Tappable.js b/server/src/models/Tappable.js index cd1f8e7ea..5d4cb5e85 100644 --- a/server/src/models/Tappable.js +++ b/server/src/models/Tappable.js @@ -71,8 +71,6 @@ class Tappable extends Model { } break } - case 's': - break default: break } From 8a5a9626cca7974e3c38593112dc8fb344aae903 Mon Sep 17 00:00:00 2001 From: Mygod Date: Wed, 15 Oct 2025 09:56:25 -0700 Subject: [PATCH 26/26] fix: missing translation --- packages/locales/lib/human/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index 0a5b9351c..8b6344cba 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -815,6 +815,7 @@ "all_stations": "All Power Spots", "gmax_stationed": "Gigantamax Placed", "search_battles": "Search Max Battles", + "search_tappables": "Search Tappables", "started": "Started", "ended": "Ended", "search_stations": "Search Power Spots",