mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
feat(web): coordinate input for asset location (#11291)
This commit is contained in:
parent
8725656fd2
commit
7d3db11a5c
5 changed files with 126 additions and 8 deletions
|
@ -0,0 +1,50 @@
|
|||
import NumberRangeInput from '$lib/components/shared-components/number-range-input.svelte';
|
||||
import { act, render, type RenderResult } from '@testing-library/svelte';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('NumberRangeInput component', () => {
|
||||
const user = userEvent.setup();
|
||||
let sut: RenderResult<NumberRangeInput>;
|
||||
let input: HTMLInputElement;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = render(NumberRangeInput, { id: '', min: -90, max: 90, onInput: () => {} });
|
||||
input = sut.getByRole('spinbutton') as HTMLInputElement;
|
||||
});
|
||||
|
||||
it('updates value', async () => {
|
||||
expect(input.value).toBe('');
|
||||
await act(() => sut.component.$set({ value: 10 }));
|
||||
expect(input.value).toBe('10');
|
||||
});
|
||||
|
||||
it('restricts minimum value', async () => {
|
||||
await user.type(input, '-91');
|
||||
expect(input.value).toBe('-90');
|
||||
});
|
||||
|
||||
it('restricts maximum value', async () => {
|
||||
await user.type(input, '09990');
|
||||
expect(input.value).toBe('90');
|
||||
});
|
||||
|
||||
it('allows entering negative numbers', async () => {
|
||||
await user.type(input, '-10');
|
||||
expect(input.value).toBe('-10');
|
||||
});
|
||||
|
||||
it('allows entering zero', async () => {
|
||||
await user.type(input, '0');
|
||||
expect(input.value).toBe('0');
|
||||
});
|
||||
|
||||
it('allows entering decimal numbers', async () => {
|
||||
await user.type(input, '-0.09001');
|
||||
expect(input.value).toBe('-0.09001');
|
||||
});
|
||||
|
||||
it('ignores text input', async () => {
|
||||
await user.type(input, 'test');
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
});
|
|
@ -12,6 +12,7 @@
|
|||
import SearchBar from '../elements/search-bar.svelte';
|
||||
import { listNavigation } from '$lib/actions/list-navigation';
|
||||
import { t } from 'svelte-i18n';
|
||||
import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte';
|
||||
|
||||
export let asset: AssetResponseDto | undefined = undefined;
|
||||
|
||||
|
@ -34,9 +35,9 @@
|
|||
confirm: Point;
|
||||
}>();
|
||||
|
||||
$: lat = asset?.exifInfo?.latitude || 0;
|
||||
$: lng = asset?.exifInfo?.longitude || 0;
|
||||
$: zoom = lat && lng ? 15 : 1;
|
||||
$: lat = asset?.exifInfo?.latitude ?? undefined;
|
||||
$: lng = asset?.exifInfo?.longitude ?? undefined;
|
||||
$: zoom = lat !== undefined && lng !== undefined ? 15 : 1;
|
||||
|
||||
$: {
|
||||
if (places) {
|
||||
|
@ -148,7 +149,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<label for="datetime">{$t('pick_a_location')}</label>
|
||||
<span>{$t('pick_a_location')}</span>
|
||||
<div class="h-[500px] min-h-[300px] w-full">
|
||||
{#await import('../shared-components/map/map.svelte')}
|
||||
{#await delay(timeToLoadTheMap) then}
|
||||
|
@ -157,10 +158,9 @@
|
|||
<LoadingSpinner />
|
||||
</div>
|
||||
{/await}
|
||||
{:then component}
|
||||
<svelte:component
|
||||
this={component.default}
|
||||
mapMarkers={lat && lng && asset
|
||||
{:then { default: Map }}
|
||||
<Map
|
||||
mapMarkers={lat !== undefined && lng !== undefined && asset
|
||||
? [
|
||||
{
|
||||
id: asset.id,
|
||||
|
@ -181,5 +181,16 @@
|
|||
/>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 gap-4 text-sm text-left mt-4">
|
||||
<CoordinatesInput
|
||||
lat={point ? point.lat : lat}
|
||||
lng={point ? point.lng : lng}
|
||||
onUpdate={(lat, lng) => {
|
||||
point = { lat, lng };
|
||||
addClipMapMarker(lng, lat);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import NumberRangeInput from '$lib/components/shared-components/number-range-input.svelte';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let lat: number | null | undefined = undefined;
|
||||
export let lng: number | null | undefined = undefined;
|
||||
export let onUpdate: (lat: number, lng: number) => void;
|
||||
|
||||
const id = generateId();
|
||||
|
||||
const onInput = () => {
|
||||
if (lat != null && lng != null) {
|
||||
onUpdate(lat, lng);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<label class="immich-form-label" for="latitude-input-{id}">{$t('latitude')}</label>
|
||||
<NumberRangeInput id="latitude-input-{id}" min={-90} max={90} {onInput} bind:value={lat} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="immich-form-label" for="longitude-input-{id}">{$t('longitude')}</label>
|
||||
<NumberRangeInput id="longitude-input-{id}" min={-180} max={180} {onInput} bind:value={lng} />
|
||||
</div>
|
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { clamp } from 'lodash-es';
|
||||
|
||||
export let id: string;
|
||||
export let min: number;
|
||||
export let max: number;
|
||||
export let step: number | string = 'any';
|
||||
export let required = true;
|
||||
export let value: number | null = null;
|
||||
export let onInput: (value: number | null) => void;
|
||||
</script>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
class="immich-form-input w-full"
|
||||
{id}
|
||||
{min}
|
||||
{max}
|
||||
{step}
|
||||
{required}
|
||||
bind:value
|
||||
on:input={() => {
|
||||
if (value !== null && (value < min || value > max)) {
|
||||
value = clamp(value, min, max);
|
||||
}
|
||||
onInput(value);
|
||||
}}
|
||||
/>
|
|
@ -740,6 +740,7 @@
|
|||
"language_setting_description": "Select your preferred language",
|
||||
"last_seen": "Last seen",
|
||||
"latest_version": "Latest Version",
|
||||
"latitude": "Latitude",
|
||||
"leave": "Leave",
|
||||
"let_others_respond": "Let others respond",
|
||||
"level": "Level",
|
||||
|
@ -786,6 +787,7 @@
|
|||
"login_has_been_disabled": "Login has been disabled.",
|
||||
"logout_all_device_confirmation": "Are you sure you want to log out all devices?",
|
||||
"logout_this_device_confirmation": "Are you sure you want to log out this device?",
|
||||
"longitude": "Longitude",
|
||||
"look": "Look",
|
||||
"loop_videos": "Loop videos",
|
||||
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
|
||||
|
|
Loading…
Reference in a new issue