mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
feat(client): show Dawarich stays, routes and marks where they belong
The trip planner and the journal get a "From Dawarich" list: the stays recorded for those dates, each one a row that can be dismissed or turned into a place or an entry after a review step that shows time, name, note and day. A place that came from a recording carries the Dawarich mark next to its name so its origin stays visible afterwards. The recorded route lies over the trip and journal maps as an overlay, coloured per day and cut along the reader's own timezone rather than UTC. It is fetched for the window on screen and never stored. The Atlas gets its own panel beside the map for the countries and cities the recordings prove, and the wish list is ticked off there, on the day of the visit, undoably. The phone has its own take on all of it, in the mobile shell's colours and with native sheets.
This commit is contained in:
@@ -8,11 +8,14 @@ import {
|
||||
Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen,
|
||||
MessageCircle, StickyNote, BarChart3, Sparkles, Luggage, Plane, Server, Cloud, Bookmark, Check, Loader2,
|
||||
} from 'lucide-react'
|
||||
import DawarichIcon from '../../../components/shared/DawarichIcon'
|
||||
import AirTrailIcon from '../../../components/shared/AirTrailIcon'
|
||||
import MToggle from '../../components/MToggle'
|
||||
import { MAdminButton, MAdminCard, MAdminField, MAdminInput, MAdminSecretInput } from './MAdminUi'
|
||||
|
||||
const ICON_MAP = {
|
||||
ListChecks, Wallet, FileText, CalendarDays, Puzzle, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, Plane, Bookmark,
|
||||
Dawarich: DawarichIcon,
|
||||
}
|
||||
|
||||
function ImmichIcon({ size = 14 }: { size?: number }) {
|
||||
@@ -57,9 +60,16 @@ interface ProviderOption {
|
||||
interface AddonIconProps {
|
||||
name: string
|
||||
size?: number
|
||||
/** A switched-off addon greys its icon out, brand marks included. */
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
function AddonIcon({ name, size = 18 }: AddonIconProps) {
|
||||
function AddonIcon({ name, size = 18, enabled = true }: AddonIconProps) {
|
||||
if (name === 'Dawarich') return <DawarichIcon fill muted={!enabled} />
|
||||
// 'Plane' is the icon string airtrail was seeded with, and INSERT OR IGNORE
|
||||
// means every existing install still carries it — so the brand is keyed on
|
||||
// that rather than on a new name no row would ever have.
|
||||
if (name === 'Plane') return <AirTrailIcon fill muted={!enabled} />
|
||||
const Icon = ICON_MAP[name] || Puzzle
|
||||
return <Icon size={size} />
|
||||
}
|
||||
@@ -304,8 +314,9 @@ function MAddonRow({ addon, onToggle, t, first }: MAddonRowProps) {
|
||||
addon.type === 'global' ? t('admin.addons.type.global') : addon.type === 'integration' ? t('admin.addons.type.integration') : t('admin.addons.type.trip')
|
||||
return (
|
||||
<div className={`flex items-center gap-3 py-[11px] ${first ? '' : 'border-t border-[color:var(--m-rowbr)]'}`}>
|
||||
<span className="flex h-[38px] w-[38px] flex-none items-center justify-center rounded-[11px] bg-[color:var(--m-ic)] text-m-ink">
|
||||
<AddonIcon name={addon.icon} size={18} />
|
||||
{/* overflow-hidden so a brand mark that fills the slot keeps its rounded corners. */}
|
||||
<span className="flex h-[38px] w-[38px] flex-none items-center justify-center overflow-hidden rounded-[11px] bg-[color:var(--m-ic)] text-m-ink">
|
||||
<AddonIcon name={addon.icon} size={18} enabled={addon.enabled} />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -10,6 +10,9 @@ import MAtlasSearch from './MAtlasSearch'
|
||||
import MAtlasCountryPopup from './MAtlasCountryPopup'
|
||||
import MAtlasBucketSheet from './MAtlasBucketSheet'
|
||||
import MToggle from '../../components/MToggle'
|
||||
import DawarichIcon from '../../../components/shared/DawarichIcon'
|
||||
import DawarichAtlasDialog from '../../../components/Dawarich/DawarichAtlasDialog'
|
||||
import { useAddonStore } from '../../../store/addonStore'
|
||||
import { countryStatus } from '../../../pages/atlas/atlasModel'
|
||||
|
||||
const removeBtnCls = 'mt-4 w-full rounded-full bg-[rgba(214,39,59,.12)] py-[11px] text-center text-[0.8125rem] font-bold text-[color:var(--m-st-danger)]' // theme-lint-disable — fixed status-danger tint
|
||||
@@ -44,6 +47,8 @@ export default function MAtlas() {
|
||||
const plannedCount = stats.totalCountriesPlanned || 0
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const [bucketOpen, setBucketOpen] = useState(false)
|
||||
const [dawarichOpen, setDawarichOpen] = useState(false)
|
||||
const dawarichEnabled = useAddonStore(state => state.isEnabled)('dawarich')
|
||||
const [detailOpen, setDetailOpen] = useState(false)
|
||||
|
||||
// The dock FAB is the search button on this screen (demo Z. 1099) and hands
|
||||
@@ -115,10 +120,27 @@ export default function MAtlas() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBucketOpen(true)}
|
||||
className="flex h-[38px] flex-1 items-center justify-center rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-sheet)] px-4 text-[0.78125rem] font-bold text-m-ink shadow-[0_5px_12px_-8px_rgba(0,0,0,.18)]"
|
||||
className="flex h-[38px] min-w-0 flex-1 items-center justify-center rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-sheet)] px-4 text-[0.78125rem] font-bold text-m-ink shadow-[0_5px_12px_-8px_rgba(0,0,0,.18)]"
|
||||
>
|
||||
{t('atlas.bucketTab')}
|
||||
</button>
|
||||
{/* Dawarich answers two questions about this map, so it opens from the
|
||||
map — not from inside the wishlist sheet, where it was one card deep
|
||||
and looked like part of the list it is only asking about. Renders
|
||||
nothing when the addon is off. */}
|
||||
{dawarichEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDawarichOpen(true)}
|
||||
aria-label={t('dawarich.title')}
|
||||
className="flex h-[38px] min-w-0 flex-1 items-center justify-center gap-2 rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-sheet)] px-3 shadow-[0_5px_12px_-8px_rgba(0,0,0,.18)]"
|
||||
>
|
||||
<span className="flex h-[26px] w-[26px] flex-none overflow-hidden rounded-full">
|
||||
<DawarichIcon size={26} />
|
||||
</span>
|
||||
<span className="truncate text-[0.78125rem] font-bold text-m-ink">{t('dawarich.title')}</span>
|
||||
</button>
|
||||
)}
|
||||
{/* Only worth the space once there is something planned to reveal. */}
|
||||
{plannedCount > 0 && (
|
||||
<div className="flex h-[38px] shrink-0 items-center gap-2 rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-sheet)] px-3 shadow-[0_5px_12px_-8px_rgba(0,0,0,.18)]">
|
||||
@@ -128,6 +150,14 @@ export default function MAtlas() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dawarichOpen && (
|
||||
<DawarichAtlasDialog
|
||||
isOpen
|
||||
onClose={() => setDawarichOpen(false)}
|
||||
onChanged={atlas.reloadAfterDawarich}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MAtlasStatsCard stats={stats} />
|
||||
|
||||
<MAtlasSearch
|
||||
|
||||
@@ -236,6 +236,7 @@ export default function MAtlasBucketSheet({ atlas, open, onClose }: MAtlasBucket
|
||||
{t('atlas.addPoi')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</MSheet>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ChevronLeft, MapPin, Grid3x3, Upload, MoreHorizontal, Play, Image, Camera, EyeOff, Settings2 } from 'lucide-react'
|
||||
import { useSearchParams } from 'react-router'
|
||||
import { ChevronLeft, MapPin, Grid3x3, MoreHorizontal, Play, Image, Camera, EyeOff, Settings2 } from 'lucide-react'
|
||||
import JourneyMap from '../../../components/Journey/JourneyMapAuto'
|
||||
import type { JourneyMapAutoHandle } from '../../../components/Journey/JourneyMapAuto'
|
||||
import PhotoLightbox from '../../../components/Journey/PhotoLightbox'
|
||||
@@ -9,6 +10,10 @@ import { ProviderPicker } from '../../../components/Journey/JourneyDetailPagePro
|
||||
import { photoUrl } from '../../../pages/journeyDetail/JourneyDetailPage.helpers'
|
||||
import { useJourneyDetail } from '../../../pages/journeyDetail/useJourneyDetail'
|
||||
import { useJourneyStore } from '../../../store/journeyStore'
|
||||
import { useAddonStore } from '../../../store/addonStore'
|
||||
import DawarichIcon from '../../../components/shared/DawarichIcon'
|
||||
import DawarichSuggestionsPanel from '../../../components/Dawarich/DawarichSuggestionsPanel'
|
||||
import { FormSheetHeader } from '../trip/sheets/PlSheetChrome'
|
||||
import type { JourneyEntry, GalleryPhoto } from '../../../store/journeyStore'
|
||||
import { useAuthStore } from '../../../store/authStore'
|
||||
import { journeyApi, addonsApi, memoriesApi } from '../../../api/client'
|
||||
@@ -43,6 +48,15 @@ export default function MJourneyDetail() {
|
||||
loadJourney, updateEntry, deleteEntry, uploadPhotos,
|
||||
} = useJourneyDetail()
|
||||
|
||||
// The dock's FAB is a sibling of this screen: on the Gallery it becomes the
|
||||
// upload button, so it has to know which tab is open — and it stops knowing
|
||||
// when this screen goes away.
|
||||
const setMobileGalleryOpen = useJourneyStore(state => state.setMobileGalleryOpen)
|
||||
useEffect(() => {
|
||||
setMobileGalleryOpen(view === 'gallery')
|
||||
return () => setMobileGalleryOpen(false)
|
||||
}, [view, setMobileGalleryOpen])
|
||||
|
||||
const mapRef = useRef<JourneyMapAutoHandle>(null)
|
||||
const carouselRef = useRef<HTMLDivElement>(null)
|
||||
const cardRefs = useRef<Map<number, HTMLDivElement>>(new Map())
|
||||
@@ -129,11 +143,45 @@ export default function MJourneyDetail() {
|
||||
// Gallery upload — device files plus the connected photo providers (Immich/Synology).
|
||||
const galleryFileRef = useRef<HTMLInputElement>(null)
|
||||
const [availableProviders, setAvailableProviders] = useState<{ id: string; name: string }[]>([])
|
||||
// Whether the probe below has finished — not whether it found anything.
|
||||
const [providersReady, setProvidersReady] = useState(false)
|
||||
const [showUploadMenu, setShowUploadMenu] = useState(false)
|
||||
const [showActionMenu, setShowActionMenu] = useState(false)
|
||||
const [pickerProvider, setPickerProvider] = useState<string | null>(null)
|
||||
const dawarichEnabled = useAddonStore(state => state.isEnabled)('dawarich')
|
||||
const [dawarichOpen, setDawarichOpen] = useState(false)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
const openUpload = useCallback(() => {
|
||||
if (availableProviders.length > 0) setShowUploadMenu(true)
|
||||
else galleryFileRef.current?.click()
|
||||
}, [availableProviders.length])
|
||||
|
||||
// The dock's FAB asks through the URL, the way every other "+" in the shell
|
||||
// does. The parameter is cleared straight away so going back does not reopen
|
||||
// the picker; the intent is held in state until the provider probe has
|
||||
// answered, because "device or Immich?" cannot be asked before we know
|
||||
// whether there is an Immich.
|
||||
const [params, setParams] = useSearchParams()
|
||||
const [pendingUpload, setPendingUpload] = useState(false)
|
||||
useEffect(() => {
|
||||
if (params.get('create') !== 'photo') return
|
||||
setParams(prev => {
|
||||
const next = new URLSearchParams(prev)
|
||||
next.delete('create')
|
||||
return next
|
||||
}, { replace: true })
|
||||
if (!canEditEntries) return
|
||||
setView('gallery')
|
||||
setPendingUpload(true)
|
||||
}, [params, setParams, canEditEntries, setView])
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingUpload || !providersReady) return
|
||||
setPendingUpload(false)
|
||||
openUpload()
|
||||
}, [pendingUpload, providersReady, openUpload])
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
;(async () => {
|
||||
@@ -155,6 +203,7 @@ export default function MJourneyDetail() {
|
||||
}
|
||||
if (active) setAvailableProviders(connected)
|
||||
} catch { /* no providers */ }
|
||||
finally { if (active) setProvidersReady(true) }
|
||||
})()
|
||||
return () => { active = false }
|
||||
}, [])
|
||||
@@ -314,20 +363,18 @@ export default function MJourneyDetail() {
|
||||
</button>
|
||||
</div>
|
||||
<span className="ml-auto flex flex-none items-center gap-2">
|
||||
{view === 'gallery' && canEditEntries && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (availableProviders.length > 0 ? setShowUploadMenu(true) : galleryFileRef.current?.click())}
|
||||
disabled={uploading}
|
||||
{/* Uploading lives on the dock's FAB while the Gallery is open — the
|
||||
one big action on the screen. A second button up here would be the
|
||||
same thing twice, so it only appears while an upload is running,
|
||||
as its progress. */}
|
||||
{view === 'gallery' && canEditEntries && uploading && (
|
||||
<span
|
||||
role="status"
|
||||
aria-label={t('common.upload')}
|
||||
className="flex h-[38px] w-[38px] items-center justify-center rounded-full bg-m-act text-m-actfg shadow-[0_5px_14px_-6px_rgba(0,0,0,.3)] disabled:opacity-60"
|
||||
className="flex h-[38px] w-[38px] items-center justify-center rounded-full bg-m-act text-m-actfg shadow-[0_5px_14px_-6px_rgba(0,0,0,.3)]"
|
||||
>
|
||||
{uploading ? (
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
) : (
|
||||
<Upload size={16} strokeWidth={2.2} />
|
||||
)}
|
||||
</button>
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
@@ -340,6 +387,47 @@ export default function MJourneyDetail() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* The stays Dawarich recorded for these dates, as candidate entries. Over
|
||||
the map rather than in the header: it belongs to what is on the map,
|
||||
and the header is already three controls wide on a phone. Sits above
|
||||
the card rail so it never covers the card somebody is reading. */}
|
||||
{dawarichEnabled && canEditEntries && view === 'timeline' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDawarichOpen(true)}
|
||||
aria-label={t('dawarich.suggestions.title')}
|
||||
className="absolute right-4 z-[9] flex h-[46px] w-[46px] items-center justify-center overflow-hidden rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-sheet)] shadow-[0_6px_18px_-8px_rgba(0,0,0,.35)] bottom-[calc(var(--bottom-nav-h,84px)+16px+128px)]"
|
||||
>
|
||||
<DawarichIcon size={46} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{dawarichOpen && current && (
|
||||
<MSheet open onClose={() => setDawarichOpen(false)} variant="bottom" material="glass" ariaLabel={t('dawarich.suggestions.title')}>
|
||||
<div className="flex max-h-[82dvh] min-h-0 flex-col">
|
||||
<FormSheetHeader
|
||||
leading={
|
||||
<span className="flex h-10 w-10 flex-none overflow-hidden rounded-[13px]">
|
||||
<DawarichIcon size={40} />
|
||||
</span>
|
||||
}
|
||||
title={t('dawarich.suggestions.title')}
|
||||
onClose={() => setDawarichOpen(false)}
|
||||
closeLabel={t('common.close')}
|
||||
/>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-[14px] pb-4">
|
||||
{/* Journal entries and nothing else: a journey is for writing, and
|
||||
the trip planner already offers the same stay as a place. */}
|
||||
<DawarichSuggestionsPanel
|
||||
bare
|
||||
journals={[{ id: current.id, label: current.title }]}
|
||||
onAccepted={() => { void loadJourney(current.id) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MSheet>
|
||||
)}
|
||||
|
||||
{/* Horizontal card timeline */}
|
||||
{view === 'timeline' && entries.length > 0 && (
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import React from 'react'
|
||||
import { Plug, Save, RefreshCw, Unplug } from 'lucide-react'
|
||||
import { useTranslation } from '../../../i18n'
|
||||
import { useDawarichConnection } from '../../../hooks/useDawarichConnection'
|
||||
import { MSetCard, MSetRow, MSetInput, MSetButton, MSetHint } from './MSettingsUi'
|
||||
import MToggle from '../../components/MToggle'
|
||||
|
||||
/**
|
||||
* Phone twin of components/Settings/DawarichConnectionSection.
|
||||
*
|
||||
* Presentation only: every piece of behaviour lives in `useDawarichConnection`,
|
||||
* which both shells share. That is not tidiness — SonarCloud allows 3%
|
||||
* duplicated lines on a PR's new code, and a second copy of the connect / test /
|
||||
* sync logic would spend the whole budget on its own.
|
||||
*/
|
||||
export default function MDawarichConnectionSection(): React.ReactElement {
|
||||
const { t, locale } = useTranslation()
|
||||
const S = useDawarichConnection()
|
||||
|
||||
return (
|
||||
<MSetCard title={t('dawarich.title')} icon={Plug} className="mt-3">
|
||||
<MSetHint className="mt-0">{t('dawarich.intro')}</MSetHint>
|
||||
|
||||
<div className="mt-3 space-y-3">
|
||||
<div>
|
||||
<MSetInput
|
||||
type="url"
|
||||
inputMode="url"
|
||||
value={S.url}
|
||||
onChange={e => S.setUrl(e.target.value)}
|
||||
placeholder="https://dawarich.example.com"
|
||||
aria-label={t('dawarich.url')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<MSetInput
|
||||
type="password"
|
||||
value={S.apiKey}
|
||||
onChange={e => S.setApiKey(e.target.value)}
|
||||
autoComplete="off"
|
||||
placeholder={S.connected && !S.apiKey ? '••••••••' : t('dawarich.apiKeyPlaceholder')}
|
||||
aria-label={t('dawarich.apiKey')}
|
||||
/>
|
||||
<MSetHint>{t('dawarich.apiKeyHint')}</MSetHint>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-1">
|
||||
<MSetRow
|
||||
first
|
||||
label={t('dawarich.syncEnabled')}
|
||||
sub={t('dawarich.syncEnabledHint')}
|
||||
trailing={
|
||||
<MToggle checked={S.syncEnabled} onChange={S.toggleSync} ariaLabel={t('dawarich.syncEnabled')} />
|
||||
}
|
||||
/>
|
||||
<MSetRow
|
||||
label={t('dawarich.allowInsecureTls')}
|
||||
sub={t('dawarich.allowInsecureTlsHint')}
|
||||
trailing={
|
||||
<MToggle
|
||||
checked={S.allowInsecureTls}
|
||||
onChange={S.toggleInsecureTls}
|
||||
ariaLabel={t('dawarich.allowInsecureTls')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
<MSetButton onClick={S.save} disabled={S.saving || S.loading || !S.canSave}>
|
||||
<Save size={14} /> {t('common.save')}
|
||||
</MSetButton>
|
||||
<MSetButton variant="ghost" onClick={S.test} disabled={S.testing || S.loading || !S.url.trim()}>
|
||||
<Plug size={14} /> {t('dawarich.test.button')}
|
||||
</MSetButton>
|
||||
{S.connected && (
|
||||
<MSetButton variant="ghost" onClick={S.syncNow} disabled={S.syncing}>
|
||||
<RefreshCw size={14} className={S.syncing ? 'animate-spin' : ''} /> {t('dawarich.syncNow')}
|
||||
</MSetButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-[4px]">
|
||||
<p className="font-geist text-[0.6875rem] font-semibold text-m-ink">
|
||||
{S.connected ? t('dawarich.connected') : t('dawarich.notConnected')}
|
||||
</p>
|
||||
{S.probeMessage && (
|
||||
<p className="font-geist text-[0.625rem] leading-relaxed text-m-muted">{S.probeMessage}</p>
|
||||
)}
|
||||
{S.connected && (
|
||||
<p className="font-geist text-[0.625rem] leading-relaxed text-m-muted">
|
||||
{S.lastSyncAt
|
||||
? t('dawarich.lastSync', { when: new Date(S.lastSyncAt).toLocaleString(locale) })
|
||||
: t('dawarich.neverSynced')}
|
||||
</p>
|
||||
)}
|
||||
{S.lastSyncError && (
|
||||
<p className="font-geist text-[0.625rem] leading-relaxed text-[color:var(--m-st-danger)]">
|
||||
{S.lastSyncError}
|
||||
</p>
|
||||
)}
|
||||
{S.capabilities?.serverVersion && (
|
||||
<p className="font-geist text-[0.625rem] leading-relaxed text-m-faint">
|
||||
{t('dawarich.serverVersion', { version: S.capabilities.serverVersion })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{S.connected && (
|
||||
<div className="mt-3">
|
||||
<MSetButton variant="danger" onClick={S.disconnect} disabled={S.saving}>
|
||||
<Unplug size={14} /> {t('dawarich.disconnect')}
|
||||
</MSetButton>
|
||||
</div>
|
||||
)}
|
||||
</MSetCard>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useEffect } from 'react'
|
||||
import { useAddonStore } from '../../../store/addonStore'
|
||||
import MPhotoProvidersSection from './MPhotoProvidersSection'
|
||||
import MAirTrailConnectionSection from './MAirTrailConnectionSection'
|
||||
import MDawarichConnectionSection from './MDawarichConnectionSection'
|
||||
import MLlmConnectionSection from './MLlmConnectionSection'
|
||||
import MSettingsMcp from './MSettingsMcp'
|
||||
import { useAuthStore } from '../../../store/authStore'
|
||||
@@ -17,6 +18,7 @@ export default function MSettingsIntegrations() {
|
||||
const mcpEnabled = addonEnabled('mcp')
|
||||
const airtrailEnabled = addonEnabled('airtrail')
|
||||
const llmEnabled = addonEnabled('llm_parsing')
|
||||
const dawarichEnabled = addonEnabled('dawarich')
|
||||
const managed = useAuthStore((s) => s.managed)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,6 +29,9 @@ export default function MSettingsIntegrations() {
|
||||
<>
|
||||
<MPhotoProvidersSection />
|
||||
{airtrailEnabled && <MAirTrailConnectionSection />}
|
||||
{/* Reaches a server the reader runs themselves, which a managed install has
|
||||
no route to — the same gate the desktop shell puts on AirTrail. */}
|
||||
{dawarichEnabled && !managed && <MDawarichConnectionSection />}
|
||||
{/* Which model reads a booking, and what that costs, comes with the instance on
|
||||
a managed install. The per-user fallback exists for people who supply their
|
||||
own key, and there nobody does. */}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||
import { MapViewAuto } from '../../../../components/Map/MapViewAuto'
|
||||
import { MapCompassPill, type CompassMap } from '../../../../components/Map/MapCompassPill'
|
||||
import { TripRouteOverviewPill, TripRouteOverviewPanel } from '../../../../components/Map/TripRouteOverview'
|
||||
import { DawarichTrailPill } from '../../../../components/Map/DawarichTrailPill'
|
||||
import PoiCategoryPill from '../../../../components/Map/PoiCategoryPill'
|
||||
import { usePoiExplore } from '../../../../components/Map/usePoiExplore'
|
||||
import { useSettingsStore } from '../../../../store/settingsStore'
|
||||
@@ -44,6 +45,7 @@ export default function MMapArea({ planner, shell }: MMapAreaProps) {
|
||||
<div className="absolute inset-0 isolate overflow-hidden bg-[color:var(--m-mapb)] [--bottom-nav-h:calc(env(safe-area-inset-bottom,0px)+74px)]">
|
||||
<MapViewAuto
|
||||
tripId={planner.tripId}
|
||||
dawarichTrack={planner.dawarichTrail.track}
|
||||
places={planner.mapPlaces}
|
||||
dayPlaces={planner.dayPlaces}
|
||||
route={planner.overviewActive ? planner.tripOverview.lines : planner.route}
|
||||
@@ -115,9 +117,9 @@ export default function MMapArea({ planner, shell }: MMapAreaProps) {
|
||||
Tailwind rather than inline because the compass band is identified by being
|
||||
the one element with an inline --bottom-nav-h, and a second would make that
|
||||
ambiguous. */}
|
||||
{mapActive && !planner.roadtripActive && (
|
||||
{mapActive && (!planner.roadtripActive || planner.dawarichEnabled) && (
|
||||
<div className="pointer-events-none absolute left-3 right-3 z-[25] flex flex-col items-end gap-2 bottom-[calc(var(--bottom-nav-h,84px)+58px)]">
|
||||
{planner.overviewActive && (
|
||||
{!planner.roadtripActive && planner.overviewActive && (
|
||||
<TripRouteOverviewPanel
|
||||
overview={planner.tripOverview}
|
||||
unit={distanceUnit}
|
||||
@@ -128,7 +130,18 @@ export default function MMapArea({ planner, shell }: MMapAreaProps) {
|
||||
maxWidth={240}
|
||||
/>
|
||||
)}
|
||||
<TripRouteOverviewPill active={planner.overviewShown} onToggle={planner.toggleOverview} />
|
||||
{!planner.roadtripActive && (
|
||||
<TripRouteOverviewPill active={planner.overviewShown} onToggle={planner.toggleOverview} />
|
||||
)}
|
||||
{/* Kept in road-trip mode: the recorded route beside the planned one is
|
||||
exactly the comparison a drive invites. */}
|
||||
{planner.dawarichEnabled && (
|
||||
<DawarichTrailPill
|
||||
active={planner.dawarichTrailShown}
|
||||
status={planner.dawarichTrail.status}
|
||||
onToggle={planner.toggleDawarichTrail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,10 @@ import { getCategoryIcon } from '../../../../components/shared/categoryIcons'
|
||||
import { resolveTrackColor } from '../../../../components/Map/trackColors'
|
||||
import MConfirmSheet from '../../settings/MConfirmSheet'
|
||||
import type { MPlacesBrowserProps } from '../MTripShell'
|
||||
import DawarichSuggestionsPanel from '../../../../components/Dawarich/DawarichSuggestionsPanel'
|
||||
import { formatDayOption } from '../../../../components/Dawarich/dawarichSuggestionModel'
|
||||
import { refreshTripAfterAccept } from '../../../../components/Dawarich/dawarichTripRefresh'
|
||||
import { useTranslation } from '../../../../i18n'
|
||||
import type { Place } from '../../../../types'
|
||||
import MPlacesBulkCategorySheet from './MPlacesBulkCategorySheet'
|
||||
import MPlacesSaveToCollectionSheet from './MPlacesSaveToCollectionSheet'
|
||||
@@ -31,6 +35,8 @@ import { filterPool, firstPlannedDayNumbers, plannedPlaceIds } from './placesBro
|
||||
*/
|
||||
export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps) {
|
||||
const { t, places, categories, assignments, days, trip } = planner
|
||||
// The planner hook carries `t` but not the locale; day labels need both.
|
||||
const { locale } = useTranslation()
|
||||
const canEditPlaces = planner.can('place_edit', trip)
|
||||
const collectionsEnabled = useAddonStore(s => s.isEnabled('collections'))
|
||||
|
||||
@@ -231,6 +237,24 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stays Dawarich recorded on these dates (#2279). Same component as the
|
||||
desktop rail — the rows are the same rows, and the panel renders
|
||||
nothing when there is nothing pending. */}
|
||||
<div className="mt-3">
|
||||
<DawarichSuggestionsPanel
|
||||
tripId={planner.tripId}
|
||||
trips={[{ id: planner.tripId, label: t('dawarich.accept.thisTrip') }]}
|
||||
daysForTrip={() => days.map(day => ({
|
||||
id: day.id,
|
||||
...formatDayOption(day.day_number, day.date, locale, t),
|
||||
}))}
|
||||
// The place it just created belongs on the map and in the list
|
||||
// now, not after a reload.
|
||||
onAccepted={() => { void refreshTripAfterAccept(planner.tripId) }}
|
||||
initiallyCollapsed
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Selection toolbar ── */}
|
||||
{selectMode && (
|
||||
<div className="mt-2 flex items-center gap-2 rounded-full border border-[color:var(--m-gbr)] bg-[color:var(--m-glass)] py-[6px] pl-[14px] pr-[6px] backdrop-blur-[20px]">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import DawarichIcon from '../../../../components/shared/DawarichIcon'
|
||||
import {
|
||||
Bookmark, Camera, ChevronRight, ExternalLink, Loader2, Map as MapIcon, Navigation, Paperclip,
|
||||
Pencil, Phone, Plus, Route, Trash2, Upload, X,
|
||||
@@ -301,7 +302,16 @@ export default function MPlaceSheet({ planner, shell }: MTripSheetsProps) {
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[1rem] font-bold leading-snug">{place.name}</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="min-w-0 truncate text-[1rem] font-bold leading-snug">{place.name}</span>
|
||||
{/* Accepted out of the traveller's own recordings — the same
|
||||
mark the desktop inspector carries. */}
|
||||
{place.source === 'dawarich' && (
|
||||
<span className="flex-none overflow-hidden rounded-[5px]">
|
||||
<DawarichIcon size={15} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{place.address && (
|
||||
<div className="mt-[2px] font-geist text-[0.6875rem] leading-[1.4] text-m-muted">{place.address}</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user