mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
feat(settings): optional preferred map app for the navigate button (#2423)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getNavigationTargets, navigationTargetLabel, openNavigationTarget, showsAppleMaps, showsGeoUri } from './placeNavigation'
|
||||
import { getNavigationTargets, navigationAppChoices, navigationTargetLabel, openNavigationTarget, preferredNavAppOptions, showsAppleMaps, showsGeoUri, withPreferredApp } from './placeNavigation'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import type { Place } from '../../types'
|
||||
|
||||
// FE-PLANNER-NAV-001 to FE-PLANNER-NAV-008
|
||||
@@ -163,3 +164,34 @@ describe('openNavigationTarget', () => {
|
||||
expect(assigned).toEqual([target.url])
|
||||
})
|
||||
})
|
||||
|
||||
describe('preferred map app (#2423)', () => {
|
||||
afterEach(() => {
|
||||
useSettingsStore.setState(s => ({ settings: { ...s.settings, preferred_nav_app: '' } }))
|
||||
})
|
||||
|
||||
it('FE-PLANNER-NAV-2423-1: unset keeps every app on offer', () => {
|
||||
expect(getNavigationTargets(place()).length).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
it('FE-PLANNER-NAV-2423-2: a picked app is the only target, so the button opens it straight away', () => {
|
||||
useSettingsStore.setState(s => ({ settings: { ...s.settings, preferred_nav_app: 'waze' } }))
|
||||
const targets = getNavigationTargets(place())
|
||||
expect(targets.map(t => t.id)).toEqual(['waze'])
|
||||
})
|
||||
|
||||
it('FE-PLANNER-NAV-2423-3: an app this place cannot be opened in falls back to the full list', () => {
|
||||
const targets = [{ id: 'google' as const, label: 'Google Maps', url: 'g' }, { id: 'osm' as const, label: 'OpenStreetMap', url: 'o' }]
|
||||
expect(withPreferredApp(targets, 'amap')).toEqual(targets)
|
||||
expect(withPreferredApp(targets, null)).toEqual(targets)
|
||||
expect(withPreferredApp(targets, 'osm')).toEqual([targets[1]])
|
||||
})
|
||||
|
||||
it('FE-PLANNER-NAV-2423-4: the settings options start with asking and name every app', () => {
|
||||
const options = preferredNavAppOptions(k => k)
|
||||
expect(options[0]).toEqual({ value: '', label: 'settings.preferredNavAppAsk' })
|
||||
expect(options.slice(1).map(o => o.value)).toEqual(navigationAppChoices().map(a => a.id))
|
||||
expect(options.map(o => o.value)).toContain('google')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getAmapUrlForPlace } from './placeAmap'
|
||||
import { getCoMapsUrlForPlace } from './placeCoMaps'
|
||||
import { getGoogleMapsUrlForPlace } from './placeGoogleMaps'
|
||||
import { getOpenStreetMapUrlForPlace } from './placeOpenStreetMap'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
|
||||
type PlaceLike = Pick<Place | AssignmentPlace, 'name' | 'address' | 'lat' | 'lng' | 'google_place_id' | 'google_ftid'>
|
||||
|
||||
@@ -80,6 +81,49 @@ export function showsAppleMaps(): boolean {
|
||||
export function getNavigationTargets(
|
||||
place: PlaceLike | null | undefined,
|
||||
detailsUrl?: string | null,
|
||||
): NavigationTarget[] {
|
||||
return withPreferredApp(allNavigationTargets(place, detailsUrl), useSettingsStore.getState().settings.preferred_nav_app)
|
||||
}
|
||||
|
||||
/**
|
||||
* The traveller's preferred map app (#2423), when they picked one in settings:
|
||||
* just that target, so every navigate button opens it straight away. Unset, or
|
||||
* an app this place cannot be opened in (Amap outside China, Waze without
|
||||
* coordinates), keeps the full list.
|
||||
*/
|
||||
export function withPreferredApp(targets: NavigationTarget[], preferred: string | null | undefined): NavigationTarget[] {
|
||||
if (!preferred) return targets
|
||||
const match = targets.find(target => target.id === preferred)
|
||||
return match ? [match] : targets
|
||||
}
|
||||
|
||||
/**
|
||||
* The apps the settings picker offers (#2423), narrowed by platform the same
|
||||
* way the list on a place is: Apple Maps not on Android, the generic geo: entry
|
||||
* only there. Amap stays in, it is simply skipped for places outside China.
|
||||
*/
|
||||
export function navigationAppChoices(): { id: NavigationAppId; label: string; labelKey?: string }[] {
|
||||
const choices: { id: NavigationAppId; label: string; labelKey?: string }[] = [
|
||||
{ id: 'google', label: 'Google Maps' },
|
||||
{ id: 'waze', label: 'Waze' },
|
||||
]
|
||||
if (showsAppleMaps()) choices.push({ id: 'apple', label: 'Apple Maps' })
|
||||
choices.push({ id: 'osm', label: 'OpenStreetMap' }, { id: 'comaps', label: 'CoMaps' }, { id: 'amap', label: '高德地图' })
|
||||
if (showsGeoUri()) choices.push({ id: 'geo', label: 'geo:', labelKey: 'inspector.otherMapApp' })
|
||||
return choices
|
||||
}
|
||||
|
||||
/** The settings options (#2423): "ask every time" first, then each app from navigationAppChoices. */
|
||||
export function preferredNavAppOptions(t: Translate): { value: string; label: string }[] {
|
||||
return [
|
||||
{ value: '', label: t('settings.preferredNavAppAsk') },
|
||||
...navigationAppChoices().map(app => ({ value: app.id, label: app.labelKey ? t(app.labelKey) : app.label })),
|
||||
]
|
||||
}
|
||||
|
||||
function allNavigationTargets(
|
||||
place: PlaceLike | null | undefined,
|
||||
detailsUrl?: string | null,
|
||||
): NavigationTarget[] {
|
||||
if (!place) return []
|
||||
const targets: NavigationTarget[] = []
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SUPPORTED_LANGUAGES, useTranslation } from '../../i18n'
|
||||
import { useSettingsStore, DEFAULT_SETTINGS } from '../../store/settingsStore'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import CustomSelect from '../shared/CustomSelect'
|
||||
import { preferredNavAppOptions } from '../Planner/placeNavigation'
|
||||
import { SYMBOLS, currenciesWith } from '../Budget/BudgetPanel.constants'
|
||||
import Section from './Section'
|
||||
import { TRIP_TAB_IDS, TRIP_TAB_LABEL_KEYS } from '../../constants/tripTabs'
|
||||
@@ -327,6 +328,20 @@ export default function DisplaySettingsTab(): React.ReactElement {
|
||||
</Section>
|
||||
|
||||
<Section title={t('settings.general.travelMap')} icon={Map}>
|
||||
{/* Preferred map app (#2423): opt-in, the picker stays the default */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.preferredNavApp')}</label>
|
||||
<CustomSelect
|
||||
value={settings.preferred_nav_app || ''}
|
||||
onChange={async v => {
|
||||
try { await updateSetting('preferred_nav_app', String(v)) }
|
||||
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
|
||||
}}
|
||||
options={preferredNavAppOptions(t)}
|
||||
/>
|
||||
<p className="text-xs text-content-faint mt-2">{t('settings.preferredNavAppHint')}</p>
|
||||
</div>
|
||||
|
||||
{/* Date first in day headings (#1953) */}
|
||||
<OnOffSetting label={t('settings.dayDateFirst')} hint={t('settings.dayDateFirstHint')} on={settings.day_date_first === true} onChange={value => saveOnOff('day_date_first', value)} />
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ import { DEFAULT_START_PAGE, DEFAULT_START_TRIP_TAB, type StartPage } from '../.
|
||||
import type { Settings, DistanceUnit, WeekStart } from '../../../types'
|
||||
import { DEFAULT_WEEK_START } from '@trek/shared'
|
||||
import { weekStartOptions } from '../../../utils/calendarWeek'
|
||||
import { MSetCard, MSetEyebrow, MSetSelectRow, MSetSegments, MSetRow } from './MSettingsUi'
|
||||
import { MSetCard, MSetEyebrow, MSetHint, MSetSelectRow, MSetSegments, MSetRow } from './MSettingsUi'
|
||||
import MToggle from '../../components/MToggle'
|
||||
import MSetPickerSheet from './MSetPickerSheet'
|
||||
import { preferredNavAppOptions } from '../../../components/Planner/placeNavigation'
|
||||
|
||||
/**
|
||||
* "General" settings section — the demo's Language & region and Travel & map
|
||||
@@ -24,6 +25,9 @@ export default function MSettingsGeneral() {
|
||||
const [currencyOpen, setCurrencyOpen] = useState(false)
|
||||
const [langOpen, setLangOpen] = useState(false)
|
||||
const [startTabOpen, setStartTabOpen] = useState(false)
|
||||
const [navAppOpen, setNavAppOpen] = useState(false)
|
||||
const navAppOptions = preferredNavAppOptions(t)
|
||||
const navAppValue = settings.preferred_nav_app || ''
|
||||
|
||||
const save = async (key: keyof Settings, value: Settings[keyof Settings]) => {
|
||||
try {
|
||||
@@ -169,6 +173,13 @@ export default function MSettingsGeneral() {
|
||||
</MSetCard>
|
||||
|
||||
<MSetCard title={t('settings.general.travelMap')} icon={Map} className="mt-3">
|
||||
<MSetEyebrow className="mb-[5px]">{t('settings.preferredNavApp')}</MSetEyebrow>
|
||||
<MSetSelectRow
|
||||
label={navAppOptions.find(o => o.value === navAppValue)?.label ?? navAppOptions[0].label}
|
||||
trailing={chevron}
|
||||
onClick={() => setNavAppOpen(true)}
|
||||
/>
|
||||
<MSetHint className="mb-[10px]">{t('settings.preferredNavAppHint')}</MSetHint>
|
||||
<div className="-mt-[6px]">
|
||||
{travelRows.map((row) => (
|
||||
<MSetRow
|
||||
@@ -208,6 +219,15 @@ export default function MSettingsGeneral() {
|
||||
options={SUPPORTED_LANGUAGES.map((l) => ({ value: l.value, label: l.label }))}
|
||||
/>
|
||||
|
||||
<MSetPickerSheet
|
||||
open={navAppOpen}
|
||||
onClose={() => setNavAppOpen(false)}
|
||||
title={t('settings.preferredNavApp')}
|
||||
value={navAppValue}
|
||||
onSelect={(v) => save('preferred_nav_app', v)}
|
||||
options={navAppOptions}
|
||||
/>
|
||||
|
||||
<MSetPickerSheet
|
||||
open={startTabOpen}
|
||||
onClose={() => setStartTabOpen(false)}
|
||||
|
||||
@@ -138,6 +138,11 @@ export interface Settings {
|
||||
blur_booking_codes?: boolean
|
||||
/** Day headings lead with the calendar date and name the trip day second (#1953). */
|
||||
day_date_first?: boolean
|
||||
/**
|
||||
* Map app every navigate button opens straight away (#2423), as a
|
||||
* NavigationAppId. Empty or absent keeps the picker with every app.
|
||||
*/
|
||||
preferred_nav_app?: string
|
||||
/** Places not planned into any day are drawn as small markers without their photo (#2024). */
|
||||
map_compact_unplanned?: boolean
|
||||
map_booking_labels?: boolean
|
||||
|
||||
Reference in New Issue
Block a user