mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-14 01:54:04 +00:00
[ENG-1097] DMS coordinate display support (#1335)
* 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
This commit is contained in:
parent
e847f2e0f4
commit
0382a4e48f
|
@ -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 }) => (
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.platform.openLink(props.url);
|
||||
}}
|
||||
>
|
||||
{props.text}
|
||||
</a>
|
||||
);
|
||||
|
||||
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' ? (
|
||||
<div className="flex flex-col gap-0 py-2">
|
||||
|
@ -40,76 +92,62 @@ function MediaData({ data }: Props) {
|
|||
<MetaData label="Type" value={data.type} />
|
||||
<MetaData
|
||||
label="Location"
|
||||
tooltipValue={
|
||||
data.location
|
||||
? `${data.location.latitude}, ${data.location.longitude}`
|
||||
: '--'
|
||||
}
|
||||
tooltipValue={data.location && formatLocation(data.location, coordinatesFormat)}
|
||||
value={
|
||||
data.location ? (
|
||||
<a
|
||||
onClick={(e) => {
|
||||
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)}
|
||||
</a>
|
||||
) : (
|
||||
'--'
|
||||
data.location && (
|
||||
<UrlMetadataValue
|
||||
url={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
|
||||
formatLocation(data.location, 'dd')
|
||||
)}`}
|
||||
text={formatLocation(
|
||||
data.location,
|
||||
coordinatesFormat,
|
||||
coordinatesFormat === 'dd' ? 4 : 0
|
||||
)}
|
||||
platform={platform}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MetaData
|
||||
label="Plus Code"
|
||||
value={
|
||||
data.location?.pluscode ? (
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (data.location)
|
||||
platform.openLink(
|
||||
`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
|
||||
data.location.pluscode
|
||||
)}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
{data.location?.pluscode}
|
||||
</a>
|
||||
) : (
|
||||
'--'
|
||||
data.location?.pluscode && (
|
||||
<UrlMetadataValue
|
||||
url={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
|
||||
data.location.pluscode
|
||||
)}`}
|
||||
text={data.location.pluscode}
|
||||
platform={platform}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MetaData
|
||||
label="Dimensions"
|
||||
value={
|
||||
<>
|
||||
{data.dimensions.width} x {data.dimensions.height}
|
||||
</>
|
||||
}
|
||||
value={`${data.dimensions.width} x ${data.dimensions.height}`}
|
||||
/>
|
||||
<MetaData label="Device" value={data.camera_data.device_make} />
|
||||
<MetaData label="Model" value={data.camera_data.device_model} />
|
||||
<MetaData
|
||||
label="Orientation"
|
||||
value={orientations[data.camera_data.orientation] ?? '--'}
|
||||
/>
|
||||
<MetaData label="Orientation" value={orientations[data.camera_data.orientation]} />
|
||||
<MetaData label="Color profile" value={data.camera_data.color_profile} />
|
||||
<MetaData label="Color space" value={data.camera_data.color_space} />
|
||||
<MetaData label="Flash" value={data.camera_data.flash?.mode} />
|
||||
<MetaData label="Zoom" value={data.camera_data.zoom?.toFixed(2)} />
|
||||
<MetaData
|
||||
label="Zoom"
|
||||
value={
|
||||
data.camera_data &&
|
||||
data.camera_data.zoom &&
|
||||
!Number.isNaN(data.camera_data.zoom)
|
||||
? `${data.camera_data.zoom.toFixed(2) + 'x'}`
|
||||
: '--'
|
||||
}
|
||||
/>
|
||||
<MetaData label="Iso" value={data.camera_data.iso} />
|
||||
<MetaData label="Software" value={data.camera_data.software} />
|
||||
</Accordion>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
};
|
||||
|
||||
export default MediaData;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<Theme['themeValue']>(
|
||||
themeStore.syncThemeWithSystem === true ? 'system' : themeStore.theme
|
||||
);
|
||||
|
@ -107,6 +116,7 @@ export const Component = () => {
|
|||
document.documentElement.style.setProperty('--dark-hue', hue.toString());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form} onSubmit={onSubmit}>
|
||||
|
@ -212,6 +222,40 @@ export const Component = () => {
|
|||
</Setting>
|
||||
</div>
|
||||
</Form>
|
||||
<Divider />
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="mb-3 text-lg font-bold text-ink">Display Formats</h1>
|
||||
|
||||
<Setting mini title="Coordinates">
|
||||
<Select
|
||||
onChange={(e) => (getUnitFormatStore().coordinatesFormat = e)}
|
||||
value={formatStore.coordinatesFormat}
|
||||
>
|
||||
<SelectOption value="dms">DMS</SelectOption>
|
||||
<SelectOption value="dd">Decimal</SelectOption>
|
||||
</Select>
|
||||
</Setting>
|
||||
|
||||
<Setting mini title="Distance">
|
||||
<Select
|
||||
onChange={(e) => (getUnitFormatStore().distanceFormat = e)}
|
||||
value={formatStore.distanceFormat}
|
||||
>
|
||||
<SelectOption value="km">Kilometers</SelectOption>
|
||||
<SelectOption value="miles">Miles</SelectOption>
|
||||
</Select>
|
||||
</Setting>
|
||||
|
||||
<Setting mini title="Temperature">
|
||||
<Select
|
||||
onChange={(e) => (getUnitFormatStore().temperatureFormat = e)}
|
||||
value={formatStore.temperatureFormat}
|
||||
>
|
||||
<SelectOption value="celsius">Celsius</SelectOption>
|
||||
<SelectOption value="fahrenheit">Fahrenheit</SelectOption>
|
||||
</Select>
|
||||
</Setting>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -10,3 +10,4 @@ export * from './useTelemetryState';
|
|||
export * from './useThemeStore';
|
||||
export * from './useNotifications';
|
||||
export * from './useForceUpdate';
|
||||
export * from './useUnitFormatStore';
|
||||
|
|
23
packages/client/src/hooks/useUnitFormatStore.tsx
Normal file
23
packages/client/src/hooks/useUnitFormatStore.tsx
Normal file
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue