mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
feat(mobile): one Maps button for every map app, actions as one glass group
The day plan offered Google Maps and CoMaps as two tiles. One Maps button now opens the day in whichever app the traveller picks: Google Maps, Apple Maps with the stops between as waypoints, CoMaps, Waze and on Android any other map app, the last two to the first stop, which they say. The preferred map app from the settings opens straight away. The day's actions sit together on one glass surface as round icons over their labels, and the up-next card wears the same surface as the timeline card under it.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { dayRouteTargets, generateAppleMapsUrl, generateGeoUrl, generateWazeUrl } from './dayRouteApps';
|
||||
|
||||
const A = { lat: 35.71, lng: 139.79, name: 'Senso-ji' };
|
||||
const B = { lat: 35.72, lng: 139.77, name: 'Ueno Park' };
|
||||
const C = { lat: 35.66, lng: 139.7, name: 'Shibuya' };
|
||||
|
||||
function userAgent(ua: string) {
|
||||
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(ua);
|
||||
}
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe('dayRouteApps', () => {
|
||||
it('opens one stop in Apple Maps as a named pin', () => {
|
||||
expect(generateAppleMapsUrl([A])).toBe('https://maps.apple.com/?ll=35.71,139.79&q=Senso-ji');
|
||||
});
|
||||
|
||||
it('hands Apple Maps the whole day, the stops between as waypoints, in the travel mode', () => {
|
||||
expect(generateAppleMapsUrl([A, B, C], 'walking')).toBe(
|
||||
'https://maps.apple.com/directions?source=35.71,139.79&destination=35.66,139.7&waypoint=35.72,139.77&mode=walking'
|
||||
);
|
||||
});
|
||||
|
||||
it('sends Waze and geo: to the first stop, since they take one place', () => {
|
||||
expect(generateWazeUrl([A, B])).toBe('https://waze.com/ul?ll=35.71,139.79&navigate=yes');
|
||||
expect(generateGeoUrl([A, B])).toBe('geo:35.71,139.79?q=35.71,139.79(Senso-ji)');
|
||||
expect(generateWazeUrl([])).toBeNull();
|
||||
});
|
||||
|
||||
it('offers Apple Maps on an iPhone and marks the one-place apps for a multi-stop day', () => {
|
||||
userAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)');
|
||||
const targets = dayRouteTargets([A, B], 'driving');
|
||||
expect(targets.map((t) => t.id)).toEqual(['google', 'apple', 'comaps', 'waze']);
|
||||
expect(targets.find((t) => t.id === 'waze')?.firstStopOnly).toBe(true);
|
||||
expect(targets.find((t) => t.id === 'google')?.firstStopOnly).toBeUndefined();
|
||||
});
|
||||
|
||||
it('offers the generic map-app link on Android instead of Apple Maps', () => {
|
||||
userAgent('Mozilla/5.0 (Linux; Android 15)');
|
||||
expect(dayRouteTargets([A], 'driving').map((t) => t.id)).toEqual(['google', 'comaps', 'waze', 'geo']);
|
||||
// A single stop is the whole day, so nothing is cut short.
|
||||
expect(dayRouteTargets([A], 'driving').some((t) => t.firstStopOnly)).toBe(false);
|
||||
});
|
||||
|
||||
it('offers nothing for a day without a located stop', () => {
|
||||
expect(dayRouteTargets([], 'driving')).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
generateCoMapsUrl,
|
||||
generateGoogleMapsUrl,
|
||||
type NamedWaypoint,
|
||||
type RouteProfileKey,
|
||||
} from '../Map/RouteCalculator';
|
||||
import { showsAppleMaps, showsGeoUri, type NavigationAppId } from './placeNavigation';
|
||||
|
||||
/**
|
||||
* The map apps a day's route can be handed to, behind the plan's one Maps
|
||||
* button. Same stops as every other export (dayExportStops), so the app shows
|
||||
* the day the plan shows.
|
||||
*
|
||||
* Not every app takes a whole route. Google Maps, Apple Maps and CoMaps do (each
|
||||
* in its own way, see the builders); Waze and the generic geo: link navigate to
|
||||
* one place only, so they lead to the first stop and say so rather than quietly
|
||||
* dropping the rest of the day.
|
||||
*/
|
||||
export interface DayRouteTarget {
|
||||
id: NavigationAppId;
|
||||
/** Product name, never translated; the generic entry carries labelKey instead. */
|
||||
label: string;
|
||||
labelKey?: string;
|
||||
url: string;
|
||||
/** True when the app only gets the first stop, not the whole route. */
|
||||
firstStopOnly?: boolean;
|
||||
}
|
||||
|
||||
/** Apple Maps' directions modes. Cycling exists there; anything else drives. */
|
||||
function appleMode(profile: RouteProfileKey): string {
|
||||
if (profile === 'walking') return 'walking';
|
||||
if (profile === 'cycling') return 'cycling';
|
||||
return 'driving';
|
||||
}
|
||||
|
||||
const point = (p: NamedWaypoint) => `${p.lat},${p.lng}`;
|
||||
const label = (p: NamedWaypoint) => encodeURIComponent(p.name?.trim() || point(p));
|
||||
|
||||
/**
|
||||
* Apple Maps for a day: one stop opens as a pin with its name, more go as
|
||||
* directions with the stops between as waypoints (Apple's unified Maps URLs,
|
||||
* which iOS, macOS and the web version all read).
|
||||
*/
|
||||
export function generateAppleMapsUrl(places: NamedWaypoint[], profile: RouteProfileKey = 'driving'): string | null {
|
||||
const valid = places.filter((p) => p.lat != null && p.lng != null);
|
||||
if (valid.length === 0) return null;
|
||||
if (valid.length === 1) return `https://maps.apple.com/?ll=${point(valid[0])}&q=${label(valid[0])}`;
|
||||
const [first, ...rest] = valid;
|
||||
const last = rest[rest.length - 1];
|
||||
const between = rest
|
||||
.slice(0, -1)
|
||||
.map((p) => `&waypoint=${point(p)}`)
|
||||
.join('');
|
||||
return `https://maps.apple.com/directions?source=${point(first)}&destination=${point(last)}${between}&mode=${appleMode(profile)}`;
|
||||
}
|
||||
|
||||
/** Waze navigates to one place; the day's first stop is where the drive starts. */
|
||||
export function generateWazeUrl(places: NamedWaypoint[]): string | null {
|
||||
const first = places.find((p) => p.lat != null && p.lng != null);
|
||||
return first ? `https://waze.com/ul?ll=${point(first)}&navigate=yes` : null;
|
||||
}
|
||||
|
||||
/** The generic Android link, handed to whichever map app is installed; one place too. */
|
||||
export function generateGeoUrl(places: NamedWaypoint[]): string | null {
|
||||
const first = places.find((p) => p.lat != null && p.lng != null);
|
||||
return first ? `geo:${point(first)}?q=${point(first)}(${label(first)})` : null;
|
||||
}
|
||||
|
||||
/** Every app the day can be opened in on this device, in the order they are offered. */
|
||||
export function dayRouteTargets(stops: NamedWaypoint[], profile: RouteProfileKey): DayRouteTarget[] {
|
||||
const out: DayRouteTarget[] = [];
|
||||
const push = (target: Omit<DayRouteTarget, 'url'>, url: string | null) => {
|
||||
if (url) out.push({ ...target, url });
|
||||
};
|
||||
push({ id: 'google', label: 'Google Maps' }, generateGoogleMapsUrl(stops));
|
||||
if (showsAppleMaps()) push({ id: 'apple', label: 'Apple Maps' }, generateAppleMapsUrl(stops, profile));
|
||||
push({ id: 'comaps', label: 'CoMaps' }, generateCoMapsUrl(stops, profile));
|
||||
const located = stops.filter((p) => p.lat != null && p.lng != null).length;
|
||||
push({ id: 'waze', label: 'Waze', firstStopOnly: located > 1 }, generateWazeUrl(stops));
|
||||
if (showsGeoUri()) {
|
||||
push(
|
||||
{ id: 'geo', label: 'geo:', labelKey: 'inspector.otherMapApp', firstStopOnly: located > 1 },
|
||||
generateGeoUrl(stops)
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, type MouseEvent } from 'react'
|
||||
import { ArrowRight, BedDouble, CalendarDays, CalendarRange, ChevronRight, Compass, LogIn, LogOut, MapPin, Pencil, PencilLine, Route, Ticket, TrainFront, Undo2 } from 'lucide-react'
|
||||
import { ArrowRight, BedDouble, CalendarDays, CalendarRange, ChevronRight, LogIn, LogOut, Map as MapIcon, MapPin, Pencil, PencilLine, Route, Ticket, TrainFront, Undo2 } from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { useContextMenu, ContextMenu } from '../../../../components/shared/ContextMenu'
|
||||
import MarkdownText from '../../../../components/shared/MarkdownText'
|
||||
@@ -22,8 +22,9 @@ import type { MPlanTimelineProps } from '../MTripShell'
|
||||
import type { MergedItem } from '../../../../utils/dayMerge'
|
||||
import type { RouteSegment } from '../../../../types'
|
||||
import type { Assignment } from '../../../../types'
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
import GoogleMapsIcon from '../../../../components/shared/GoogleMapsIcon'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useSettingsStore } from '../../../../store/settingsStore'
|
||||
import MRouteAppsSheet, { openRouteTarget } from './MRouteAppsSheet'
|
||||
import MDayPager from '../../../components/MDayPager'
|
||||
|
||||
/**
|
||||
@@ -181,11 +182,23 @@ function PlanDayPage({ planner, shell, dayId: pageDayId, active, editing, legMen
|
||||
|
||||
const chrome = { editing, t, language: tl.language, timeFormat: tl.timeFormat }
|
||||
|
||||
// One Maps button for every app the day can be opened in: the preferred app
|
||||
// from settings (#2423) opens straight away, otherwise the sheet asks.
|
||||
const [mapsOpen, setMapsOpen] = useState(false)
|
||||
const preferredNavApp = useSettingsStore(s => s.settings.preferred_nav_app)
|
||||
const openMaps = () => {
|
||||
const preferred = preferredNavApp ? tl.routeTargets.find(target => target.id === preferredNavApp) : undefined
|
||||
if (preferred) openRouteTarget(preferred)
|
||||
else setMapsOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
// One column under the day chips: the UP-NEXT card (go mode) or the edit
|
||||
// header, then the timeline card filling the rest, 12px apart whatever height
|
||||
// the card above it has. A day without an up-next starts the timeline right
|
||||
// under the chips.
|
||||
<>
|
||||
<MRouteAppsSheet open={mapsOpen} onClose={() => setMapsOpen(false)} targets={tl.routeTargets} t={t} />
|
||||
{/* One column under the day chips: the UP-NEXT card (go mode) or the edit
|
||||
header, then the timeline card filling the rest, 12px apart whatever height
|
||||
the card above it has. A day without an up-next starts the timeline right
|
||||
under the chips. */}
|
||||
<div className="absolute inset-x-4 flex flex-col gap-3 top-[calc(var(--m-safe-top,12px)+102px)] bottom-[calc(env(safe-area-inset-bottom,0px)+90px)]">
|
||||
{!editing && <UpNextCard tl={tl} t={t} onOpen={openPlace} />}
|
||||
{editing && <EditHeader tl={tl} planner={planner} shell={shell} />}
|
||||
@@ -308,7 +321,9 @@ function PlanDayPage({ planner, shell, dayId: pageDayId, active, editing, legMen
|
||||
)}
|
||||
|
||||
{editing && (
|
||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||
// The day's actions as one group on one glass surface, the way iOS groups
|
||||
// its controls: each a round icon over its label.
|
||||
<div className="mt-3 grid grid-cols-3 gap-x-1 gap-y-2 rounded-[22px] border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] p-2 shadow-[0_8px_24px_-16px_rgba(0,0,0,.35)] backdrop-blur-[24px] backdrop-saturate-[1.7]">
|
||||
<PlanAction icon={MapPin} label={t('mobileTrip.addPlaceShort')} onClick={tl.addPlace} />
|
||||
<PlanAction
|
||||
icon={PencilLine}
|
||||
@@ -318,16 +333,14 @@ function PlanDayPage({ planner, shell, dayId: pageDayId, active, editing, legMen
|
||||
<PlanAction icon={Ticket} label={t('mobileTrip.addBookingShort')} onClick={tl.addBooking} />
|
||||
<PlanAction icon={TrainFront} label={t('mobileTrip.addTransportShort')} onClick={tl.addTransport} />
|
||||
<PlanAction icon={Route} label={t('dayplan.optimize')} onClick={() => void tl.optimize()} />
|
||||
{tl.canExportRoute && (
|
||||
<>
|
||||
<PlanAction icon={GoogleMapsIcon} label={t('mobileTrip.googleMaps')} onClick={tl.exportGoogleMaps} />
|
||||
<PlanAction icon={Compass} label={t('mobileTrip.coMaps')} onClick={tl.exportCoMaps} />
|
||||
</>
|
||||
{tl.canExportRoute && tl.routeTargets.length > 0 && (
|
||||
<PlanAction icon={MapIcon} label={t('mobileTrip.maps')} onClick={openMaps} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -347,7 +360,9 @@ function UpNextCard({ tl, t, onOpen }: {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpen(upNext.assignment)}
|
||||
className="flex-none cursor-pointer rounded-[22px] border border-[color:var(--m-inbr)] bg-[color:var(--m-inner)] px-4 py-3.5 text-start shadow-[0_18px_44px_-18px_rgba(0,0,0,.3)] backdrop-blur-[28px] backdrop-saturate-[1.8]"
|
||||
// The same surface as the timeline card under it: the two are one plan, and
|
||||
// the place at the top plus the round arrow already say which stop is next.
|
||||
className="flex-none cursor-pointer rounded-[22px] border border-[color:var(--m-cbr)] bg-[color:var(--m-card)] px-4 py-3.5 text-start backdrop-blur-[24px] backdrop-saturate-[1.6] transition-transform active:scale-[.99]"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="whitespace-nowrap font-geist text-[0.65625rem] font-bold uppercase tracking-[.08em] text-m-muted">
|
||||
@@ -536,10 +551,10 @@ function HotelChipIcon({ variant }: { variant: 'checkout' | 'checkin' | 'stay' }
|
||||
return <BedDouble size={12} strokeWidth={2.2} className="text-m-muted" />
|
||||
}
|
||||
|
||||
/** Edit-mode action tile — Place · Note · Booking · Transport · Optimize · Google Maps, three per row. */
|
||||
/** One action of the edit-mode group: Place, Note, Booking, Transport, Optimize, Maps,
|
||||
* three per row, a round icon over its label. */
|
||||
function PlanAction({ icon: Icon, label, onClick }: {
|
||||
// Google Maps hands in its own brand mark, which is not a lucide icon (#2005).
|
||||
icon: LucideIcon | ComponentType<{ size?: number; className?: string }>
|
||||
icon: LucideIcon
|
||||
label: string
|
||||
onClick: () => void
|
||||
}) {
|
||||
@@ -547,10 +562,12 @@ function PlanAction({ icon: Icon, label, onClick }: {
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex min-w-0 flex-col items-center justify-center gap-1.5 rounded-[14px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-1 py-2.5 transition-transform active:scale-[.97]"
|
||||
className="group flex min-w-0 flex-col items-center justify-center gap-1.5 rounded-[16px] px-1 py-2 transition-transform active:scale-[.94]"
|
||||
>
|
||||
<Icon size={17} strokeWidth={2} className="text-m-muted" />
|
||||
<span className="max-w-full text-center font-geist text-[0.625rem] font-semibold leading-tight text-m-ink">{label}</span>
|
||||
<span className="flex h-11 w-11 items-center justify-center rounded-full bg-[color:var(--m-ic)] text-m-ink transition-colors group-active:bg-[color:var(--m-rowbr)]">
|
||||
<Icon size={19} strokeWidth={2} />
|
||||
</span>
|
||||
<span className="max-w-full text-center font-geist text-[0.6875rem] font-semibold leading-tight text-m-ink">{label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ChevronRight, MapPinned } from 'lucide-react';
|
||||
import type { DayRouteTarget } from '../../../../components/Planner/dayRouteApps';
|
||||
import type { TranslationFn } from '../../../../types';
|
||||
import MSheet from '../../../components/MSheet';
|
||||
|
||||
/** Opens a target: a web link in a new tab (the app takes it over), geo: in place. */
|
||||
export function openRouteTarget(target: DayRouteTarget): void {
|
||||
if (target.url.startsWith('geo:')) window.location.assign(target.url);
|
||||
else window.open(target.url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
/**
|
||||
* The plan's one Maps button, asked: which app should get the day. A docked
|
||||
* sheet of the apps this device can open, each a full-width row; the ones that
|
||||
* take a single place say they only get the first stop.
|
||||
*/
|
||||
export default function MRouteAppsSheet({
|
||||
open,
|
||||
onClose,
|
||||
targets,
|
||||
t,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
targets: DayRouteTarget[];
|
||||
t: TranslationFn;
|
||||
}) {
|
||||
return (
|
||||
<MSheet open={open} onClose={onClose} variant="bottom" ariaLabel={t('mobileTrip.openRouteIn')}>
|
||||
<div className="px-4 pb-4 pt-6">
|
||||
<div className="mb-3 flex items-center gap-2 px-1 text-[0.8125rem] font-bold text-m-ink">
|
||||
<MapPinned size={16} strokeWidth={2.2} className="text-m-muted" aria-hidden="true" />
|
||||
{t('mobileTrip.openRouteIn')}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{targets.map((target) => (
|
||||
<button
|
||||
key={target.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
openRouteTarget(target);
|
||||
onClose();
|
||||
}}
|
||||
className="flex items-center gap-3 rounded-[18px] border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] px-4 py-3 text-start transition-transform active:scale-[.98]"
|
||||
>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-[0.9375rem] font-bold text-m-ink">
|
||||
{target.labelKey ? t(target.labelKey) : target.label}
|
||||
</span>
|
||||
{target.firstStopOnly && (
|
||||
<span className="mt-0.5 block font-geist text-[0.6875rem] text-m-muted">
|
||||
{t('mobileTrip.firstStopOnly')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<ChevronRight size={17} strokeWidth={2} className="flex-none text-m-faint" aria-hidden="true" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</MSheet>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { dayWeatherAnchor } from '../../../../utils/dayWeather'
|
||||
import { metadataWithLegPositions, planMergedOrder, withTransportPositions } from '../../../../utils/mergedOrder'
|
||||
import { getDisplayTimeForDay, getMergedItems, getTransportForDay, hasCarrierEndpointOnDay, isCarrierTransport, stopDayIds, yieldTimedBookings } from '../../../../utils/dayMerge'
|
||||
import { dayCoMapsUrl, dayExportStops, dayGoogleMapsUrl, optimizeDayOrder, type DayCarrier } from '../../../../components/Planner/dayRoute'
|
||||
import { dayRouteTargets } from '../../../../components/Planner/dayRouteApps'
|
||||
import type { PlannedTransitLeg, TransitLeg } from '../../../../components/Planner/transitLeg'
|
||||
import { useTransitLegPlanner } from '../../../../components/Planner/useTransitLegPlanner'
|
||||
import { dayItemOverrun } from '../../../../components/Tours/tourDayTime'
|
||||
@@ -316,6 +317,17 @@ export function useMPlanTimeline(planner: TripPlanner, forDayId?: number | null)
|
||||
[day, days, dayAssignments, tripAccommodations, settings, dayCarrier],
|
||||
)
|
||||
|
||||
// The apps behind the plan's one Maps button, with the same bookended stops.
|
||||
const routeTargets = useMemo(
|
||||
() => (day
|
||||
? dayRouteTargets(
|
||||
dayExportStops(day, days, dayAssignments, tripAccommodations, settings.optimize_from_accommodation !== false, dayCarrier),
|
||||
day.default_transport_mode ?? routeProfile,
|
||||
)
|
||||
: []),
|
||||
[day, days, dayAssignments, tripAccommodations, settings, dayCarrier, routeProfile],
|
||||
)
|
||||
|
||||
const exportGoogleMaps = useCallback(() => {
|
||||
if (!day) return
|
||||
// Bookend the exported route with the day's accommodation the same way the
|
||||
@@ -401,7 +413,7 @@ export function useMPlanTimeline(planner: TripPlanner, forDayId?: number | null)
|
||||
moveRow, removeAssignment, editAssignment, editTransport, openTransitJourney,
|
||||
moveRowTo,
|
||||
addPlace, addBooking, addTransport,
|
||||
optimize, canExportRoute, exportGoogleMaps, exportCoMaps, renameDay, fullPlaceOf,
|
||||
optimize, canExportRoute, routeTargets, exportGoogleMaps, exportCoMaps, renameDay, fullPlaceOf,
|
||||
routeModeOptions, setLegMode, transitLegFor, planTransitLeg, journeyLate, tourOverrun,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ function UpNext({ planner, shell, rt, stageDayId, onOpen }: {
|
||||
const targets = place ? getNavigationTargets(place) : []
|
||||
|
||||
return (
|
||||
<section className="mt-2.5 rounded-[22px] border border-[color:var(--m-inbr)] bg-[color:var(--m-inner)] px-4 py-3.5 shadow-[0_18px_44px_-18px_rgba(0,0,0,.3)]">
|
||||
<section className="mt-2.5 rounded-[22px] border border-[color:var(--m-cbr)] bg-[color:var(--m-card)] px-4 py-3.5">
|
||||
<button type="button" onClick={() => onOpen(next.row)} className="w-full text-start">
|
||||
<span className="flex items-center justify-between gap-2">
|
||||
<span className="font-geist text-[0.65625rem] font-bold uppercase tracking-[.08em] text-m-muted">
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { PlanRow, TransitMeta, TransportEntry } from '../../../../src/mobil
|
||||
import type { MergedItem } from '../../../../src/utils/dayMerge'
|
||||
import type { Assignment, Day, DayNote, Place, RouteSegment } from '../../../../src/types'
|
||||
import MPlanTimeline from '../../../../src/mobile/screens/trip/plan/MPlanTimeline'
|
||||
import { resetAllStores, seedStore } from '../../../helpers/store'
|
||||
import { useSettingsStore } from '../../../../src/store/settingsStore'
|
||||
|
||||
// FE-MOB-PLTL-001 to FE-MOB-PLTL-049
|
||||
|
||||
@@ -113,6 +115,11 @@ function buildTl(over: Record<string, unknown> = {}): MPlanTimelineController {
|
||||
canExportRoute: true,
|
||||
exportGoogleMaps: vi.fn(),
|
||||
exportCoMaps: vi.fn(),
|
||||
routeTargets: [
|
||||
{ id: 'google', label: 'Google Maps', url: 'https://www.google.com/maps/dir/1,2/3,4' },
|
||||
{ id: 'comaps', label: 'CoMaps', url: 'https://comaps.at/route?x' },
|
||||
{ id: 'waze', label: 'Waze', url: 'https://waze.com/ul?ll=1,2&navigate=yes', firstStopOnly: true },
|
||||
],
|
||||
renameDay: vi.fn(),
|
||||
fullPlaceOf: vi.fn(() => undefined as Place | undefined),
|
||||
routeModeOptions: [
|
||||
@@ -145,6 +152,7 @@ function renderTimeline(
|
||||
|
||||
describe('MPlanTimeline', () => {
|
||||
beforeEach(() => {
|
||||
resetAllStores()
|
||||
mocks.schedule = EMPTY_SCHEDULE
|
||||
})
|
||||
|
||||
@@ -625,23 +633,46 @@ describe('MPlanTimeline', () => {
|
||||
fireEvent.click(screen.getByText('mobileTrip.addBookingShort'))
|
||||
fireEvent.click(screen.getByText('mobileTrip.addTransportShort'))
|
||||
fireEvent.click(screen.getByText('dayplan.optimize'))
|
||||
fireEvent.click(screen.getByText('mobileTrip.googleMaps'))
|
||||
|
||||
expect(mocks.tl.addPlace).toHaveBeenCalledTimes(1)
|
||||
expect(shell.openSheet).toHaveBeenCalledWith('note', { dayId: 2 })
|
||||
expect(mocks.tl.addBooking).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.tl.addTransport).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.tl.optimize).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.tl.exportGoogleMaps).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('FE-MOB-PLTL-049: a day with no route to hand over offers no Google Maps or CoMaps tile (#2476)', () => {
|
||||
it('FE-MOB-PLTL-033b: one Maps tile asks which app gets the day, and the pick opens it', async () => {
|
||||
const open = vi.spyOn(window, 'open').mockReturnValue(null)
|
||||
renderTimeline({}, {}, { mode: 'edit' })
|
||||
|
||||
fireEvent.click(screen.getByText('mobileTrip.maps'))
|
||||
const sheet = await screen.findByRole('dialog', { name: 'mobileTrip.openRouteIn' })
|
||||
// The app that only takes one place says so.
|
||||
expect(within(sheet).getByText('mobileTrip.firstStopOnly')).toBeInTheDocument()
|
||||
fireEvent.click(within(sheet).getByText('Google Maps'))
|
||||
|
||||
expect(open).toHaveBeenCalledWith('https://www.google.com/maps/dir/1,2/3,4', '_blank', 'noopener,noreferrer')
|
||||
open.mockRestore()
|
||||
})
|
||||
|
||||
it('FE-MOB-PLTL-033c: with a preferred map app set, the Maps tile opens it straight away (#2423)', () => {
|
||||
const open = vi.spyOn(window, 'open').mockReturnValue(null)
|
||||
seedStore(useSettingsStore, { settings: { preferred_nav_app: 'comaps' } })
|
||||
renderTimeline({}, {}, { mode: 'edit' })
|
||||
|
||||
fireEvent.click(screen.getByText('mobileTrip.maps'))
|
||||
|
||||
expect(open).toHaveBeenCalledWith('https://comaps.at/route?x', '_blank', 'noopener,noreferrer')
|
||||
expect(screen.queryByRole('dialog', { name: 'mobileTrip.openRouteIn' })).not.toBeInTheDocument()
|
||||
open.mockRestore()
|
||||
})
|
||||
|
||||
it('FE-MOB-PLTL-049: a day with no route to hand over offers no Maps tile (#2476)', () => {
|
||||
// A moving day whose only content is the flight: the export would be a drive
|
||||
// from one hotel to the other, so the two tiles are left out, not left dead.
|
||||
// from one hotel to the other, so the tile is left out, not left dead.
|
||||
renderTimeline({ canExportRoute: false }, {}, { mode: 'edit' })
|
||||
|
||||
expect(screen.queryByText('mobileTrip.googleMaps')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('mobileTrip.coMaps')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('mobileTrip.maps')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('dayplan.optimize')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@@ -148,6 +148,13 @@ describe('useMPlanTimeline', () => {
|
||||
expect(weatherApi.get).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-MOB-PLTL-005b: offers the map apps for the selected day, and none without one', async () => {
|
||||
const { result } = await renderTimeline(makePlanner())
|
||||
expect(result.current.routeTargets.map(t => t.id)).toEqual(expect.arrayContaining(['google', 'comaps', 'waze']))
|
||||
const none = await renderTimeline(makePlanner({ selectedDayId: null }))
|
||||
expect(none.result.current.routeTargets).toEqual([])
|
||||
})
|
||||
|
||||
it('FE-MOB-PLTL-006: keeps every day-scoped action inert without a selected day', async () => {
|
||||
const planner = makePlanner({ selectedDayId: null })
|
||||
const { result } = await renderTimeline(planner)
|
||||
|
||||
@@ -112,5 +112,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'في {count} مقطعًا كانت النتائج أكثر مما تتسع له إجابة واحدة. اختر أنواعًا أقل لرؤية الباقي.',
|
||||
'mobileTrip.rtNoDay': 'لم يتم اختيار يوم',
|
||||
'mobileTrip.rtNoDayHint': 'تعرض الخريطة الرحلة كاملة. اضغط على يوم في الأعلى لعرض مسيره.',
|
||||
'mobileTrip.maps': 'الخرائط',
|
||||
'mobileTrip.openRouteIn': 'افتح اليوم في',
|
||||
'mobileTrip.firstStopOnly': 'المحطة الأولى فقط',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -84,6 +84,9 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count} marşrut hissəsində bir cavaba sığmayacaq qədər nəticə var. Qalanlarını görmək üçün daha az növ seçin.',
|
||||
'mobileTrip.rtNoDay': 'Gün seçilməyib',
|
||||
'mobileTrip.rtNoDayHint': 'Xəritə bütün səyahəti göstərir. Avtomobil marşrutunu görmək üçün yuxarıdan bir gün seçin.',
|
||||
'mobileTrip.maps': 'Xəritələr',
|
||||
'mobileTrip.openRouteIn': 'Günü burada aç',
|
||||
'mobileTrip.firstStopOnly': 'Yalnız ilk dayanacaq',
|
||||
};
|
||||
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -84,5 +84,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count} trechos tinham mais do que cabe em uma resposta. Escolha menos tipos para ver o resto.',
|
||||
'mobileTrip.rtNoDay': 'Nenhum dia escolhido',
|
||||
'mobileTrip.rtNoDayHint': 'O mapa mostra a viagem inteira. Toque num dia acima para ver o seu percurso.',
|
||||
'mobileTrip.maps': 'Mapas',
|
||||
'mobileTrip.openRouteIn': 'Abrir o dia em',
|
||||
'mobileTrip.firstStopOnly': 'Só a primeira parada',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -84,6 +84,9 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count} tram tenia més del que cap en una resposta. Tria menys tipus per veure la resta.',
|
||||
'mobileTrip.rtNoDay': 'Cap dia seleccionat',
|
||||
'mobileTrip.rtNoDayHint': 'El mapa mostra tot el viatge. Toca un dia a dalt per veure’n el trajecte.',
|
||||
'mobileTrip.maps': 'Mapes',
|
||||
'mobileTrip.openRouteIn': 'Obre el dia a',
|
||||
'mobileTrip.firstStopOnly': 'Només la primera parada',
|
||||
};
|
||||
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -92,5 +92,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'Na {count} úsecích bylo více výsledků, než se vejde do jedné odpovědi. Vyberte méně druhů, abyste viděli zbytek.',
|
||||
'mobileTrip.rtNoDay': 'Není vybrán žádný den',
|
||||
'mobileTrip.rtNoDayHint': 'Mapa zobrazuje celou cestu. Klepněte nahoře na den a zobrazí se jeho trasa.',
|
||||
'mobileTrip.maps': 'Mapy',
|
||||
'mobileTrip.openRouteIn': 'Otevřít den v',
|
||||
'mobileTrip.firstStopOnly': 'Jen první zastávka',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -84,5 +84,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'Auf {count} Abschnitt gab es mehr, als in eine Antwort passt. Wähl weniger Arten, um den Rest zu sehen.',
|
||||
'mobileTrip.rtNoDay': 'Kein Tag gewählt',
|
||||
'mobileTrip.rtNoDayHint': 'Die Karte zeigt die ganze Reise. Tippe oben auf einen Tag, um seine Fahrt zu sehen.',
|
||||
'mobileTrip.maps': 'Karten',
|
||||
'mobileTrip.openRouteIn': 'Tag öffnen in',
|
||||
'mobileTrip.firstStopOnly': 'Nur der erste Halt',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -82,5 +82,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'mobileTrip.rtTruncated.one': '{count} stretch had more than fits in one answer. Pick fewer kinds to see the rest.',
|
||||
'mobileTrip.rtNoDay': 'No day picked',
|
||||
'mobileTrip.rtNoDayHint': 'The map is showing the whole trip. Tap a day above to see its drive.',
|
||||
'mobileTrip.maps': 'Maps',
|
||||
'mobileTrip.openRouteIn': 'Open the day in',
|
||||
'mobileTrip.firstStopOnly': 'First stop only',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -84,5 +84,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count} tramo tenía más de lo que cabe en una respuesta. Elige menos tipos para ver el resto.',
|
||||
'mobileTrip.rtNoDay': 'Ningún día seleccionado',
|
||||
'mobileTrip.rtNoDayHint': 'El mapa muestra todo el viaje. Toca un día arriba para ver su ruta.',
|
||||
'mobileTrip.maps': 'Mapas',
|
||||
'mobileTrip.openRouteIn': 'Abrir el día en',
|
||||
'mobileTrip.firstStopOnly': 'Solo la primera parada',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -84,5 +84,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'Ühel lõigul oli rohkem tulemusi, kui vastusesse mahub. Ülejäänute nägemiseks vali vähem liike.',
|
||||
'mobileTrip.rtNoDay': 'Päev valimata',
|
||||
'mobileTrip.rtNoDayHint': 'Kaardil on kogu reis. Selle päeva sõidu nägemiseks puuduta ülal päeva.',
|
||||
'mobileTrip.maps': 'Kaardid',
|
||||
'mobileTrip.openRouteIn': 'Ava päev rakenduses',
|
||||
'mobileTrip.firstStopOnly': 'Ainult esimene peatus',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -84,5 +84,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'Sur {count} tronçon, il y en avait plus que ce qu’une réponse peut contenir. Choisissez moins de types pour voir le reste.',
|
||||
'mobileTrip.rtNoDay': 'Aucun jour sélectionné',
|
||||
'mobileTrip.rtNoDayHint': 'La carte affiche tout le voyage. Touchez un jour ci-dessus pour voir son trajet.',
|
||||
'mobileTrip.maps': 'Cartes',
|
||||
'mobileTrip.openRouteIn': 'Ouvrir la journée dans',
|
||||
'mobileTrip.firstStopOnly': 'Premier arrêt seulement',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -84,5 +84,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'1 τμήμα είχε περισσότερα από όσα χωρούν σε μία απάντηση. Διαλέξτε λιγότερα είδη για να δείτε τα υπόλοιπα.',
|
||||
'mobileTrip.rtNoDay': 'Δεν επιλέχθηκε ημέρα',
|
||||
'mobileTrip.rtNoDayHint': 'Ο χάρτης δείχνει ολόκληρο το ταξίδι. Πατήστε μια ημέρα παραπάνω για τη διαδρομή της.',
|
||||
'mobileTrip.maps': 'Χάρτες',
|
||||
'mobileTrip.openRouteIn': 'Άνοιγμα της ημέρας σε',
|
||||
'mobileTrip.firstStopOnly': 'Μόνο η πρώτη στάση',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -88,6 +88,9 @@ const mobileTrip: TranslationStrings = {
|
||||
'mobileTrip.rtTruncated.two': 'בשני קטעים היו יותר תוצאות ממה שנכנס בתשובה אחת. בחרו פחות סוגים כדי לראות את השאר.',
|
||||
'mobileTrip.rtNoDay': 'לא נבחר יום',
|
||||
'mobileTrip.rtNoDayHint': 'המפה מציגה את כל הטיול. הקישו על יום למעלה כדי לראות את הנסיעה שלו.',
|
||||
'mobileTrip.maps': 'מפות',
|
||||
'mobileTrip.openRouteIn': 'פתיחת היום ב־',
|
||||
'mobileTrip.firstStopOnly': 'רק העצירה הראשונה',
|
||||
};
|
||||
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -84,5 +84,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count} szakaszon több találat volt, mint amennyi egy válaszba belefér. Válassz kevesebb fajtát, hogy lásd a többit.',
|
||||
'mobileTrip.rtNoDay': 'Nincs kiválasztott nap',
|
||||
'mobileTrip.rtNoDayHint': 'A térkép a teljes utat mutatja. Koppints fent egy napra a napi útvonalhoz.',
|
||||
'mobileTrip.maps': 'Térképek',
|
||||
'mobileTrip.openRouteIn': 'Nap megnyitása ebben',
|
||||
'mobileTrip.firstStopOnly': 'Csak az első megálló',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -76,5 +76,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count} ruas punya lebih banyak hasil daripada yang muat dalam satu jawaban. Pilih lebih sedikit jenis untuk melihat sisanya.',
|
||||
'mobileTrip.rtNoDay': 'Belum ada hari yang dipilih',
|
||||
'mobileTrip.rtNoDayHint': 'Peta menampilkan seluruh perjalanan. Ketuk hari di atas untuk melihat rutenya.',
|
||||
'mobileTrip.maps': 'Peta',
|
||||
'mobileTrip.openRouteIn': 'Buka hari ini di',
|
||||
'mobileTrip.firstStopOnly': 'Hanya perhentian pertama',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -84,5 +84,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'Su {count} tratto ce n’erano più di quanti ne stiano in una risposta. Scegli meno tipi per vedere il resto.',
|
||||
'mobileTrip.rtNoDay': 'Nessun giorno selezionato',
|
||||
'mobileTrip.rtNoDayHint': 'La mappa mostra l’intero viaggio. Tocca un giorno qui sopra per vederne il percorso.',
|
||||
'mobileTrip.maps': 'Mappe',
|
||||
'mobileTrip.openRouteIn': 'Apri la giornata in',
|
||||
'mobileTrip.firstStopOnly': 'Solo la prima tappa',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -74,5 +74,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'mobileTrip.rtTruncated.other': '{count} 区間で一度に返せる件数を超えました。種類を減らすと残りを確認できます。',
|
||||
'mobileTrip.rtNoDay': '日が選択されていません',
|
||||
'mobileTrip.rtNoDayHint': '地図には旅程全体が表示されています。上の日をタップするとその日の行程が見られます。',
|
||||
'mobileTrip.maps': 'マップ',
|
||||
'mobileTrip.openRouteIn': 'この日を開くアプリ',
|
||||
'mobileTrip.firstStopOnly': '最初の立ち寄り先のみ',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -75,5 +75,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count}개 구간은 결과가 너무 많아 한 번에 모두 표시하지 못했습니다. 종류를 줄이면 나머지도 볼 수 있습니다.',
|
||||
'mobileTrip.rtNoDay': '선택된 날이 없습니다',
|
||||
'mobileTrip.rtNoDayHint': '지도에 전체 여행이 표시됩니다. 위에서 날짜를 눌러 해당 일정을 확인하세요.',
|
||||
'mobileTrip.maps': '지도',
|
||||
'mobileTrip.openRouteIn': '이 날을 열 앱',
|
||||
'mobileTrip.firstStopOnly': '첫 번째 장소만',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -83,5 +83,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count} stuk had meer dan in één antwoord past. Kies minder soorten om de rest te zien.',
|
||||
'mobileTrip.rtNoDay': 'Geen dag gekozen',
|
||||
'mobileTrip.rtNoDayHint': 'De kaart toont de hele reis. Tik hierboven op een dag om de rit te zien.',
|
||||
'mobileTrip.maps': 'Kaarten',
|
||||
'mobileTrip.openRouteIn': 'Dag openen in',
|
||||
'mobileTrip.firstStopOnly': 'Alleen de eerste stop',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -100,5 +100,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'Na {count} odcinkach było więcej, niż mieści się w jednej odpowiedzi. Wybierz mniej rodzajów, by zobaczyć resztę.',
|
||||
'mobileTrip.rtNoDay': 'Nie wybrano dnia',
|
||||
'mobileTrip.rtNoDayHint': 'Mapa pokazuje całą podróż. Dotknij dnia powyżej, aby zobaczyć jego trasę.',
|
||||
'mobileTrip.maps': 'Mapy',
|
||||
'mobileTrip.openRouteIn': 'Otwórz dzień w',
|
||||
'mobileTrip.firstStopOnly': 'Tylko pierwszy przystanek',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -100,5 +100,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'На {count} участках нашлось больше, чем помещается в один ответ. Выберите меньше видов, чтобы увидеть остальное.',
|
||||
'mobileTrip.rtNoDay': 'День не выбран',
|
||||
'mobileTrip.rtNoDayHint': 'На карте вся поездка. Нажмите на день выше, чтобы увидеть его маршрут.',
|
||||
'mobileTrip.maps': 'Карты',
|
||||
'mobileTrip.openRouteIn': 'Открыть день в',
|
||||
'mobileTrip.firstStopOnly': 'Только первая остановка',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -92,5 +92,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count} úseky mali viac, než sa zmestí do jednej odpovede. Vyberte menej druhov, aby ste videli zvyšok.',
|
||||
'mobileTrip.rtNoDay': 'Nie je vybraný žiadny deň',
|
||||
'mobileTrip.rtNoDayHint': 'Mapa zobrazuje celú cestu. Ťuknite hore na deň a zobrazí sa jeho trasa.',
|
||||
'mobileTrip.maps': 'Mapy',
|
||||
'mobileTrip.openRouteIn': 'Otvoriť deň v',
|
||||
'mobileTrip.firstStopOnly': 'Len prvá zastávka',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -83,5 +83,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count} sträcka hade mer än vad som får plats i ett svar. Välj färre sorter för att se resten.',
|
||||
'mobileTrip.rtNoDay': 'Ingen dag vald',
|
||||
'mobileTrip.rtNoDayHint': 'Kartan visar hela resan. Tryck på en dag ovanför för att se dess körning.',
|
||||
'mobileTrip.maps': 'Kartor',
|
||||
'mobileTrip.openRouteIn': 'Öppna dagen i',
|
||||
'mobileTrip.firstStopOnly': 'Bara första stoppet',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -74,5 +74,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'มี {count} ช่วงที่มีผลลัพธ์มากเกินกว่าจะแสดงในคำตอบเดียว เลือกประเภทให้น้อยลงเพื่อดูที่เหลือ',
|
||||
'mobileTrip.rtNoDay': 'ยังไม่ได้เลือกวัน',
|
||||
'mobileTrip.rtNoDayHint': 'แผนที่แสดงทั้งการเดินทาง แตะวันด้านบนเพื่อดูเส้นทางขับรถของวันนั้น',
|
||||
'mobileTrip.maps': 'แผนที่',
|
||||
'mobileTrip.openRouteIn': 'เปิดวันนี้ใน',
|
||||
'mobileTrip.firstStopOnly': 'เฉพาะจุดแรก',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -83,5 +83,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count} kesimde tek bir yanıta sığandan fazlası vardı. Geri kalanı görmek için daha az tür seçin.',
|
||||
'mobileTrip.rtNoDay': 'Gün seçilmedi',
|
||||
'mobileTrip.rtNoDayHint': 'Harita tüm yolculuğu gösteriyor. Günün sürüşünü görmek için yukarıdan bir güne dokunun.',
|
||||
'mobileTrip.maps': 'Haritalar',
|
||||
'mobileTrip.openRouteIn': 'Günü şurada aç',
|
||||
'mobileTrip.firstStopOnly': 'Yalnızca ilk durak',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -100,5 +100,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'На {count} ділянках знайшлося більше, ніж уміщується в одну відповідь. Виберіть менше видів, щоб побачити решту.',
|
||||
'mobileTrip.rtNoDay': 'День не вибрано',
|
||||
'mobileTrip.rtNoDayHint': 'На мапі вся подорож. Торкніться дня вгорі, щоб побачити його маршрут.',
|
||||
'mobileTrip.maps': 'Карти',
|
||||
'mobileTrip.openRouteIn': 'Відкрити день у',
|
||||
'mobileTrip.firstStopOnly': 'Лише перша зупинка',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -76,5 +76,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'{count} đoạn có nhiều kết quả hơn mức một phản hồi chứa được. Chọn ít loại hơn để xem phần còn lại.',
|
||||
'mobileTrip.rtNoDay': 'Chưa chọn ngày',
|
||||
'mobileTrip.rtNoDayHint': 'Bản đồ đang hiển thị cả chuyến đi. Chạm vào một ngày ở trên để xem chặng lái.',
|
||||
'mobileTrip.maps': 'Bản đồ',
|
||||
'mobileTrip.openRouteIn': 'Mở ngày này trong',
|
||||
'mobileTrip.firstStopOnly': 'Chỉ điểm dừng đầu tiên',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -72,5 +72,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'mobileTrip.rtTruncated.other': '{count} 段路的結果超出一次回答能容納的量。少選幾類就能看到其餘的。',
|
||||
'mobileTrip.rtNoDay': '未選擇日期',
|
||||
'mobileTrip.rtNoDayHint': '地圖顯示的是整個行程。點按上方的日期查看當天路線。',
|
||||
'mobileTrip.maps': '地圖',
|
||||
'mobileTrip.openRouteIn': '在以下應用程式開啟當天',
|
||||
'mobileTrip.firstStopOnly': '僅第一站',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
@@ -72,5 +72,8 @@ const mobileTrip: TranslationStrings = {
|
||||
'mobileTrip.rtTruncated.other': '有 {count} 段路的结果超出单次查询上限。少选几类就能看到其余的。',
|
||||
'mobileTrip.rtNoDay': '未选择日期',
|
||||
'mobileTrip.rtNoDayHint': '地图显示的是整个行程。点按上方的日期查看当天路线。',
|
||||
'mobileTrip.maps': '地图',
|
||||
'mobileTrip.openRouteIn': '在以下应用中打开当天',
|
||||
'mobileTrip.firstStopOnly': '仅第一站',
|
||||
};
|
||||
export default mobileTrip;
|
||||
|
||||
Reference in New Issue
Block a user