[ENG-934] EXIF UI (#1305)

* Media data UI

* Make `MediaTime` adjacently tagged

* cleanup ts

* don't destructure accordion props

* Large bruh

* round location coords

---------

Co-authored-by: Oscar Beaumont <oscar@otbeaumont.me>
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
This commit is contained in:
ameer2468
2023-09-06 16:51:15 +03:00
committed by GitHub
parent f0a66c7a6d
commit 00a9f129cd
5 changed files with 106 additions and 30 deletions

BIN
Cargo.lock generated
View File

Binary file not shown.

View File

@@ -174,8 +174,6 @@ pub fn router(node: Arc<Node>) -> Router<()> {
lru_entry
};
println!("Serving from: {:?}", serve_from); // TODO
match serve_from {
ServeFrom::Local => {
let metadata = file_path_full_path.metadata().map_err(internal_server_error)?;

View File

@@ -0,0 +1,58 @@
import { MediaLocation, MediaMetadata, MediaTime } from '@sd/client';
import Accordion from '~/components/Accordion';
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;
return null;
}
function formatLocation(loc: MediaLocation): string {
// Stackoverflow says the `+` strips the trailing zeros or something so it's important, I think
return `${+loc.latitude.toFixed(2)}, ${+loc.longitude.toFixed(2)}`;
}
function MediaData({ data }: Props) {
return data.type === 'Image' ? (
<div className="flex flex-col gap-0 py-2">
<Accordion
containerClassName="flex flex-col gap-1 px-4 rounded-b-none"
titleClassName="px-4 pt-0 pb-1 !justify-end gap-2 flex-row-reverse text-ink-dull"
className="rounded-none border-0 bg-transparent py-0"
title="More info"
>
<MetaData label="Date" value={formatMediaTime(data.date_taken)} />
<MetaData label="Type" value={data.type} />
<MetaData
label="Location"
value={data.location ? formatLocation(data.location) : null}
/>
<MetaData
label="Dimensions"
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={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} />
<MetaData label="Iso" value={data.camera_data.iso} />
<MetaData label="Software" value={data.camera_data.software} />
</Accordion>
</div>
) : null;
}
export default MediaData;

View File

@@ -26,10 +26,12 @@ import {
} from 'react';
import {
type ExplorerItem,
ObjectKindEnum,
byteSize,
getExplorerItemData,
getItemFilePath,
getItemObject,
useBridgeQuery,
useItemsAsObjects,
useLibraryQuery
} from '@sd/client';
@@ -42,6 +44,7 @@ import { FileThumb } from '../FilePath/Thumb';
import { useExplorerStore } from '../store';
import { uniqueId, useExplorerItemData } from '../util';
import FavoriteButton from './FavoriteButton';
import MediaData from './MediaData';
import Note from './Note';
export const InfoPill = tw.span`inline border border-transparent px-1 text-[11px] font-medium shadow shadow-app-shade/5 bg-app-selected rounded-md text-ink-dull`;
@@ -156,16 +159,26 @@ const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
enabled: !!objectData && readyToFetch
});
let { data: fileFullPath } = useLibraryQuery(['files.getPath', objectData?.id ?? -1], {
const filePath = useLibraryQuery(['files.getPath', objectData?.id ?? -1], {
enabled: !!objectData && readyToFetch
});
if (fileFullPath == null) {
switch (item.type) {
case 'Location':
case 'NonIndexedPath':
fileFullPath = item.item.path;
//Images are only supported currently - kind = 5
const filesMediaData = useLibraryQuery(['files.getMediaData', objectData?.id ?? -1], {
enabled: objectData?.kind === ObjectKindEnum.Image && !isNonIndexed && readyToFetch
});
const ephemeralLocationMediaData = useBridgeQuery(
['files.getEphemeralMediaData', isNonIndexed ? item.item.path : ''],
{
enabled: isNonIndexed && item.item.kind === 5 && readyToFetch
}
);
const mediaData = filesMediaData ?? ephemeralLocationMediaData ?? null;
if (filePath.data == null && item.type === 'NonIndexedPath') {
filePath.data = item.item.path;
}
const { name, isDir, kind, size, casId, dateCreated, dateAccessed, dateModified, dateIndexed } =
@@ -173,10 +186,11 @@ const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
const pubId = object?.data ? uniqueId(object?.data) : null;
let extension, integrityChecksum;
const filePathItem = getItemFilePath(item);
let extension, integrityChecksum;
if (filePathItem) {
extension = 'extension' in filePathItem ? filePathItem.extension : null;
extension = filePathItem.extension;
integrityChecksum =
'integrity_checksum' in filePathItem ? filePathItem.integrity_checksum : null;
}
@@ -227,20 +241,15 @@ const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
<MetaData
icon={Path}
label="Path"
value={fileFullPath}
value={filePath.data}
onClick={() => {
// TODO: Add toast notification
fileFullPath && navigator.clipboard.writeText(fileFullPath);
filePath.data && navigator.clipboard.writeText(filePath.data);
}}
/>
</MetaContainer>
<Divider />
{
// TODO: Call `files.getMediaData` for indexed locations when we have media data UI
// TODO: Call `files.getEphemeralMediaData` for ephemeral locations when we have media data UI
}
{mediaData.data && <MediaData data={mediaData.data} />}
<MetaContainer className="flex !flex-row flex-wrap gap-1 overflow-hidden">
<InfoPill>{isDir ? 'Folder' : kind}</InfoPill>
@@ -436,19 +445,19 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => {
};
interface MetaDataProps {
icon: Icon;
icon?: Icon;
label: string;
value: ReactNode;
onClick?: () => void;
}
const MetaData = ({ icon: Icon, label, value, onClick }: MetaDataProps) => {
export const MetaData = ({ icon: Icon, label, value, onClick }: MetaDataProps) => {
return (
<div className="flex items-center text-xs text-ink-dull" onClick={onClick}>
<Icon weight="bold" className="mr-2 shrink-0" />
{Icon && <Icon weight="bold" className="mr-2 shrink-0" />}
<span className="mr-2 flex-1 whitespace-nowrap">{label}</span>
<Tooltip label={value} asChild>
<span className="truncate break-all text-ink">{value || '--'}</span>
<span className="truncate break-all text-ink">{value ?? '--'}</span>
</Tooltip>
</div>
);

View File

@@ -1,30 +1,41 @@
import clsx from 'clsx';
import { CaretDown } from 'phosphor-react';
import { useState } from 'react';
import { PropsWithChildren, useState } from 'react';
interface Props {
children: React.ReactNode;
className?: string;
title: string;
titleClassName?: string;
containerClassName?: string;
caretSize?: number;
}
const Accordion = ({ title, className, children }: Props) => {
const Accordion = (props: PropsWithChildren<Props>) => {
const [toggle, setToggle] = useState(false);
return (
<div className={clsx(className, 'rounded-md border border-app-line bg-app-darkBox')}>
<div className={clsx(props.className, 'rounded-md border border-app-line bg-app-darkBox')}>
<div
onClick={() => setToggle((t) => !t)}
className="flex items-center justify-between px-3 py-2"
className={clsx(
'flex flex-row items-center justify-between px-3 py-2',
props.titleClassName
)}
>
<p className="text-xs">{title}</p>
<p className="text-xs">{props.title}</p>
<CaretDown
size={props.caretSize || 12}
className={clsx(toggle && 'rotate-180', 'transition-all duration-200')}
/>
</div>
{toggle && (
<div className="rounded-b-md border-t border-app-line bg-app-box p-3 pt-2">
{children}
<div
className={clsx(
'rounded-b-md border-t border-app-line bg-app-box p-3 py-2',
props.containerClassName
)}
>
{props.children}
</div>
)}
</div>