From fed667ccd88d45536e101d969fd525b05cff03cf Mon Sep 17 00:00:00 2001 From: jake <77554505+brxken128@users.noreply.github.com> Date: Tue, 19 Sep 2023 09:46:14 +0100 Subject: [PATCH] [ENG-1097] DMS coordinate display support (#1335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * offer DD and DMS coordinate displays * clean up & optimise the conversion functions * add support for changing between dd/dms (and some other example changeable formats) * even further cleanup * auto format * dedicated clickable component to clean things up * slim it down by passing platform directly * make dist/temp settable, use dedicated format store * use freedom units if locale is `en-US` 🦅 * rename the store and attempt to make it more typesafe * cleanup mainly * DD -> "Decimal" in the UI and swap the options as DMS will be the default * remove useless schema * only show S decimal on hover for DMS * show `x` after zoom if it's a valid number --- .../Explorer/Inspector/MediaData.tsx | 158 +++++++++++------- .../$libraryId/Explorer/Inspector/index.tsx | 8 +- .../$libraryId/settings/client/appearance.tsx | 52 +++++- interface/app/onboarding/context.tsx | 9 + packages/client/src/hooks/index.ts | 1 + .../client/src/hooks/useUnitFormatStore.tsx | 23 +++ 6 files changed, 183 insertions(+), 68 deletions(-) create mode 100644 packages/client/src/hooks/useUnitFormatStore.tsx diff --git a/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx b/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx index b6dd1cfd4..2cf7f758c 100644 --- a/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx @@ -1,23 +1,74 @@ -import { MediaLocation, MediaMetadata, MediaTime, Orientation } from '@sd/client'; - +import { + CoordinatesFormat, + MediaLocation, + MediaMetadata, + MediaTime, + useUnitFormatStore +} from '@sd/client'; import Accordion from '~/components/Accordion'; -import { usePlatform } from '~/util/Platform'; +import { Platform, usePlatform } from '~/util/Platform'; + import { MetaData } from './index'; interface Props { data: MediaMetadata; } -function formatMediaTime(loc: MediaTime): string | null { - if (loc === 'Undefined') return null; - if ('Utc' in loc) return loc.Utc; - if ('Naive' in loc) return loc.Naive; +const formatMediaTime = (time: MediaTime): string | null => { + if (time === 'Undefined') return null; + if ('Utc' in time) return time.Utc; + if ('Naive' in time) return time.Naive; return null; -} +}; -function formatLocation(loc: MediaLocation, dp: number): string { - return `${loc.latitude.toFixed(dp)}, ${loc.longitude.toFixed(dp)}`; -} +const formatLocationDD = (loc: MediaLocation, dp?: number): string => { + // the lack of a + here will mean that coordinates may have padding at the end + // google does the same (if one is larger than the other, the smaller one will be padded with zeroes) + return `${loc.latitude.toFixed(dp ?? 8)}, ${loc.longitude.toFixed(dp ?? 8)}`; +}; + +const formatLocationDMS = (loc: MediaLocation, dp?: number): string => { + const formatCoordinatesAsDMS = ( + coordinates: number, + positiveChar: string, + negativeChar: string + ): string => { + const abs = getAbsoluteDecimals(coordinates); + const d = Math.trunc(coordinates); + const m = Math.trunc(60 * abs); + // adding 0.05 before rounding and truncating with `toFixed` makes it match up with google + const s = (abs * 3600 - m * 60 + 0.05).toFixed(dp ?? 1); + const sign = coordinates > 0 ? positiveChar : negativeChar; + return `${d}°${m}'${s}"${sign}`; + }; + + return `${formatCoordinatesAsDMS(loc.latitude, 'N', 'S')} ${formatCoordinatesAsDMS( + loc.longitude, + 'E', + 'W' + )}`; +}; + +const getAbsoluteDecimals = (num: number): number => { + const x = num.toString(); + // this becomes +0.xxxxxxxxx and is needed to convert the minutes/seconds for DMS + return Math.abs(Number.parseFloat('0.' + x.substring(x.indexOf('.') + 1))); +}; + +const formatLocation = (loc: MediaLocation, format: CoordinatesFormat, dp?: number): string => { + return format === 'dd' ? formatLocationDD(loc, dp) : formatLocationDMS(loc, dp); +}; + +const UrlMetadataValue = (props: { text: string; url: string; platform: Platform }) => ( + { + e.preventDefault(); + props.platform.openLink(props.url); + }} + > + {props.text} + +); const orientations = { Normal: 'Normal', @@ -30,8 +81,9 @@ const orientations = { CW270: 'Rotated 270° clockwise' }; -function MediaData({ data }: Props) { +const MediaData = ({ data }: Props) => { const platform = usePlatform(); + const coordinatesFormat = useUnitFormatStore().coordinatesFormat; return data.type === 'Image' ? (
@@ -40,76 +92,62 @@ function MediaData({ data }: Props) { { - e.preventDefault(); - if (data.location) - platform.openLink( - `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent( - data.location.latitude - )}%2c${encodeURIComponent(data.location.longitude)}` - ); - }} - > - {formatLocation(data.location, 3)} - - ) : ( - '--' + data.location && ( + ) } /> { - e.preventDefault(); - if (data.location) - platform.openLink( - `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent( - data.location.pluscode - )}` - ); - }} - > - {data.location?.pluscode} - - ) : ( - '--' + data.location?.pluscode && ( + ) } /> - {data.dimensions.width} x {data.dimensions.height} - - } + value={`${data.dimensions.width} x ${data.dimensions.height}`} /> - + - +
) : null; -} +}; export default MediaData; diff --git a/interface/app/$libraryId/Explorer/Inspector/index.tsx b/interface/app/$libraryId/Explorer/Inspector/index.tsx index 4e45ce710..3f453f7e3 100644 --- a/interface/app/$libraryId/Explorer/Inspector/index.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/index.tsx @@ -1,6 +1,3 @@ -import { Image, Image_Light } from '@sd/assets/icons'; -import clsx from 'clsx'; -import dayjs from 'dayjs'; import { Barcode, CircleWavyCheck, @@ -15,6 +12,9 @@ import { Path, Snowflake } from '@phosphor-icons/react'; +import { Image, Image_Light } from '@sd/assets/icons'; +import clsx from 'clsx'; +import dayjs from 'dayjs'; import { forwardRef, useCallback, @@ -36,10 +36,10 @@ import { type ExplorerItem } from '@sd/client'; import { Button, Divider, DropdownMenu, Tooltip, tw } from '@sd/ui'; - import AssignTagMenuItems from '~/components/AssignTagMenuItems'; import { useIsDark } from '~/hooks'; import { isNonEmpty } from '~/util'; + import { useExplorerContext } from '../Context'; import { FileThumb } from '../FilePath/Thumb'; import { useQuickPreviewStore } from '../QuickPreview/store'; diff --git a/interface/app/$libraryId/settings/client/appearance.tsx b/interface/app/$libraryId/settings/client/appearance.tsx index 2b291049a..2820c2a83 100644 --- a/interface/app/$libraryId/settings/client/appearance.tsx +++ b/interface/app/$libraryId/settings/client/appearance.tsx @@ -1,11 +1,18 @@ +import { CheckCircle } from '@phosphor-icons/react'; import clsx from 'clsx'; import { useMotionValueEvent, useScroll } from 'framer-motion'; -import { CheckCircle } from '@phosphor-icons/react'; import { useEffect, useRef, useState } from 'react'; -import { getThemeStore, Themes, useThemeStore, useZodForm } from '@sd/client'; -import { Button, Form, SwitchField, z } from '@sd/ui'; - +import { + getThemeStore, + getUnitFormatStore, + Themes, + useThemeStore, + useUnitFormatStore, + useZodForm +} from '@sd/client'; +import { Button, Divider, Form, Select, SelectOption, SwitchField, z } from '@sd/ui'; import { usePlatform } from '~/util/Platform'; + import { Heading } from '../Layout'; import Setting from '../Setting'; @@ -56,6 +63,8 @@ const themes: Theme[] = [ export const Component = () => { const { lockAppTheme } = usePlatform(); const themeStore = useThemeStore(); + const formatStore = useUnitFormatStore(); + const [selectedTheme, setSelectedTheme] = useState( themeStore.syncThemeWithSystem === true ? 'system' : themeStore.theme ); @@ -107,6 +116,7 @@ export const Component = () => { document.documentElement.style.setProperty('--dark-hue', hue.toString()); } }; + return ( <>
@@ -212,6 +222,40 @@ export const Component = () => {
+ +
+

Display Formats

+ + + + + + + + + + + + +
); }; diff --git a/interface/app/onboarding/context.tsx b/interface/app/onboarding/context.tsx index ecea64b25..17326c52c 100644 --- a/interface/app/onboarding/context.tsx +++ b/interface/app/onboarding/context.tsx @@ -3,9 +3,12 @@ import { createContext, useContext } from 'react'; import { useNavigate } from 'react-router'; import { currentLibraryCache, + DistanceFormat, getOnboardingStore, + getUnitFormatStore, resetOnboardingStore, telemetryStore, + TemperatureFormat, useBridgeMutation, useCachedLibraries, useMultiZodForm, @@ -74,6 +77,12 @@ const useFormState = () => { const queryClient = useQueryClient(); const submitPlausibleEvent = usePlausibleEvent(); + if (window.navigator.language === 'en-US') { + // not perfect as some linux users use en-US by default, same w/ windows + getUnitFormatStore().distanceFormat = 'miles'; + getUnitFormatStore().temperatureFormat = 'fahrenheit'; + } + const createLibrary = useBridgeMutation('library.create'); const submit = handleSubmit( diff --git a/packages/client/src/hooks/index.ts b/packages/client/src/hooks/index.ts index 73bfd6a3d..2c75cfdc1 100644 --- a/packages/client/src/hooks/index.ts +++ b/packages/client/src/hooks/index.ts @@ -10,3 +10,4 @@ export * from './useTelemetryState'; export * from './useThemeStore'; export * from './useNotifications'; export * from './useForceUpdate'; +export * from './useUnitFormatStore'; diff --git a/packages/client/src/hooks/useUnitFormatStore.tsx b/packages/client/src/hooks/useUnitFormatStore.tsx new file mode 100644 index 000000000..11b9e7c99 --- /dev/null +++ b/packages/client/src/hooks/useUnitFormatStore.tsx @@ -0,0 +1,23 @@ +import { useSnapshot } from 'valtio'; + +import { valtioPersist } from '../lib'; + +export type CoordinatesFormat = 'dms' | 'dd'; +export type DistanceFormat = 'km' | 'miles'; +export type TemperatureFormat = 'celsius' | 'fahrenheit'; + +const unitFormatStore = valtioPersist('sd-display-units', { + // these are the defaults as 99% of users would want to see them this way + // if the `en-US` locale is detected during onboarding, the distance/temp are changed to freedom units + coordinatesFormat: 'dms' as CoordinatesFormat, + distanceFormat: 'km' as DistanceFormat, + temperatureFormat: 'celsius' as TemperatureFormat +}); + +export function useUnitFormatStore() { + return useSnapshot(unitFormatStore); +} + +export function getUnitFormatStore() { + return unitFormatStore; +}