mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
[ENG-1028] Media data for ephemeral locations (#1287)
Introducing new getEphemeralMediaData Fixing some minor stuff Running pnpm format Co-authored-by: jake <77554505+brxken128@users.noreply.github.com>
This commit is contained in:
parent
881608e9a8
commit
3b8541ef54
|
@ -1,4 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en" class="vanilla-theme">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
|
|
@ -19,23 +19,29 @@ export function getDocsNavigation(docs: Doc[]): DocsNavigation {
|
|||
|
||||
const docsNavigation: DocsNavigation = [];
|
||||
|
||||
const docsBySection = coreDocs.reduce((acc, doc) => {
|
||||
const section = doc.section;
|
||||
acc[section] = acc[section] || [];
|
||||
acc[section].push(doc);
|
||||
return acc;
|
||||
}, {} as Record<string, CoreContent<Doc>[]>);
|
||||
const docsBySection = coreDocs.reduce(
|
||||
(acc, doc) => {
|
||||
const section = doc.section;
|
||||
acc[section] = acc[section] || [];
|
||||
acc[section].push(doc);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, CoreContent<Doc>[]>
|
||||
);
|
||||
|
||||
// console.log('docsBySection', docsBySection);
|
||||
|
||||
for (const section in docsBySection) {
|
||||
const docs = docsBySection[section];
|
||||
const docsByCategory = docs.reduce((acc, doc) => {
|
||||
const category = doc.category;
|
||||
acc[category] = acc[category] || [];
|
||||
acc[category].push(doc);
|
||||
return acc;
|
||||
}, {} as Record<string, CoreContent<Doc>[]>);
|
||||
const docsByCategory = docs.reduce(
|
||||
(acc, doc) => {
|
||||
const category = doc.category;
|
||||
acc[category] = acc[category] || [];
|
||||
acc[category].push(doc);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, CoreContent<Doc>[]>
|
||||
);
|
||||
|
||||
// console.log('docsByCategory', docsByCategory);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html class="vanilla-theme">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
|
|
@ -14,20 +14,29 @@ use crate::{
|
|||
copy::FileCopierJobInit, cut::FileCutterJobInit, delete::FileDeleterJobInit,
|
||||
erase::FileEraserJobInit,
|
||||
},
|
||||
media::media_data_image_from_prisma_data,
|
||||
media::{
|
||||
media_data_extractor::{
|
||||
can_extract_media_data_for_image, extract_media_data, MediaDataError,
|
||||
},
|
||||
media_data_image_from_prisma_data,
|
||||
},
|
||||
},
|
||||
prisma::{file_path, location, object},
|
||||
util::{db::maybe_missing, error::FileIOError},
|
||||
};
|
||||
|
||||
use std::path::Path;
|
||||
use sd_file_ext::{extensions::ImageExtension, kind::ObjectKind};
|
||||
use sd_media_metadata::MediaMetadata;
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
use futures::future::join_all;
|
||||
use regex::Regex;
|
||||
use rspc::{alpha::AlphaRouter, ErrorCode};
|
||||
use sd_file_ext::kind::ObjectKind;
|
||||
use sd_media_metadata::MediaMetadata;
|
||||
use serde::Deserialize;
|
||||
use specta::Type;
|
||||
use tokio::{fs, io};
|
||||
|
@ -78,6 +87,35 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|||
})
|
||||
})
|
||||
})
|
||||
.procedure("getEphemeralMediaData", {
|
||||
R.query(|_, full_path: PathBuf| async move {
|
||||
let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// TODO(fogodev): change this when we have media data for audio and videos
|
||||
let image_extension = ImageExtension::from_str(extension).map_err(|e| {
|
||||
error!("Failed to parse image extension: {e:#?}");
|
||||
rspc::Error::new(ErrorCode::BadRequest, "Invalid image extension".to_string())
|
||||
})?;
|
||||
|
||||
if !can_extract_media_data_for_image(&image_extension) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match extract_media_data(full_path).await {
|
||||
Ok(img_media_data) => Ok(Some(MediaMetadata::Image(Box::new(img_media_data)))),
|
||||
Err(MediaDataError::MediaData(sd_media_metadata::Error::NoExifDataOnPath(
|
||||
_,
|
||||
))) => Ok(None),
|
||||
Err(e) => Err(rspc::Error::with_cause(
|
||||
ErrorCode::InternalServerError,
|
||||
"Failed to extract media data".to_string(),
|
||||
e,
|
||||
)),
|
||||
}
|
||||
})
|
||||
})
|
||||
.procedure("getPath", {
|
||||
R.with2(library())
|
||||
.query(|(_, library), id: i32| async move {
|
||||
|
|
|
@ -862,7 +862,7 @@ pub(super) async fn generate_thumbnail(
|
|||
}
|
||||
// Otherwise we good, thumbnail doesn't exist so we can generate it
|
||||
} else {
|
||||
debug!(
|
||||
trace!(
|
||||
"Skipping thumbnail generation for {} because it already exists",
|
||||
path.display()
|
||||
);
|
||||
|
|
|
@ -182,6 +182,9 @@ pub async fn generate_image_thumbnail<P: AsRef<Path>>(
|
|||
img = orientation.correct_thumbnail(img);
|
||||
}
|
||||
}
|
||||
Err(sd_media_metadata::Error::NoExifDataOnSlice) => {
|
||||
// No can do if we don't have exif data
|
||||
}
|
||||
Err(e) => warn!("Unable to extract EXIF: {:?}", e),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
use std::{num::ParseFloatError, path::PathBuf};
|
||||
use std::{
|
||||
num::ParseFloatError,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("there was an i/o error")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("there was an i/o error {0} at {}", .1.display())]
|
||||
Io(std::io::Error, Box<Path>),
|
||||
#[error("error from the exif crate: {0}")]
|
||||
Exif(#[from] exif::Error),
|
||||
#[error("there was an error while parsing time with chrono: {0}")]
|
||||
|
|
|
@ -15,7 +15,10 @@ pub struct ExifReader(Exif);
|
|||
impl ExifReader {
|
||||
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
|
||||
exif::Reader::new()
|
||||
.read_from_container(&mut BufReader::new(File::open(&path)?))
|
||||
.read_from_container(&mut BufReader::new(
|
||||
File::open(&path)
|
||||
.map_err(|e| Error::Io(e, path.as_ref().to_path_buf().into_boxed_path()))?,
|
||||
))
|
||||
.map_or_else(
|
||||
|_| Err(Error::NoExifDataOnPath(path.as_ref().to_path_buf())),
|
||||
|reader| Ok(Self(reader)),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
|
|
@ -110,7 +110,7 @@ export const FileThumb = memo((props: ThumbProps) => {
|
|||
break;
|
||||
|
||||
case ThumbType.Thumbnail:
|
||||
if (itemData.thumbnailKey) {
|
||||
if (itemData.thumbnailKey.length > 0) {
|
||||
setSrc(platform.getThumbnailUrlByThumbKey(itemData.thumbnailKey));
|
||||
} else {
|
||||
setThumbType(ThumbType.Icon);
|
||||
|
|
|
@ -232,6 +232,11 @@ const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
|
|||
|
||||
<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
|
||||
}
|
||||
|
||||
<MetaContainer className="flex !flex-row flex-wrap gap-1 overflow-hidden">
|
||||
<InfoPill>{isDir ? 'Folder' : kind}</InfoPill>
|
||||
|
||||
|
|
|
@ -353,14 +353,17 @@ export default ({ children }: { children: RenderItem }) => {
|
|||
// Sets active item to selected item with least index.
|
||||
// Might seem kinda weird but it's the same behaviour as Finder.
|
||||
activeItem.current =
|
||||
allSelected.reduce((least, current) => {
|
||||
const currentItem = getElementItem(current);
|
||||
if (!currentItem) return least;
|
||||
allSelected.reduce(
|
||||
(least, current) => {
|
||||
const currentItem = getElementItem(current);
|
||||
if (!currentItem) return least;
|
||||
|
||||
if (!least) return currentItem;
|
||||
if (!least) return currentItem;
|
||||
|
||||
return currentItem.index < least.index ? currentItem : least;
|
||||
}, null as ReturnType<typeof getElementItem>)?.data ?? null;
|
||||
return currentItem.index < least.index ? currentItem : least;
|
||||
},
|
||||
null as ReturnType<typeof getElementItem>
|
||||
)?.data ?? null;
|
||||
}}
|
||||
onScroll={({ direction }) => {
|
||||
selecto.current?.findSelectableTargets();
|
||||
|
@ -436,14 +439,17 @@ export default ({ children }: { children: RenderItem }) => {
|
|||
|
||||
const elements = [...e.added, ...e.removed];
|
||||
|
||||
const items = elements.reduce((items, el) => {
|
||||
const item = getElementItem(el);
|
||||
const items = elements.reduce(
|
||||
(items, el) => {
|
||||
const item = getElementItem(el);
|
||||
|
||||
if (!item) return items;
|
||||
if (!item) return items;
|
||||
|
||||
columns.add(item.column);
|
||||
return [...items, item];
|
||||
}, [] as NonNullable<ReturnType<typeof getElementItem>>[]);
|
||||
columns.add(item.column);
|
||||
return [...items, item];
|
||||
},
|
||||
[] as NonNullable<ReturnType<typeof getElementItem>>[]
|
||||
);
|
||||
|
||||
if (columns.size > 1) {
|
||||
items.sort((a, b) => a.column - b.column);
|
||||
|
|
|
@ -89,7 +89,7 @@ export const createDefaultExplorerSettings = <TOrder extends Ordering>({
|
|||
contentId: 180,
|
||||
objectId: 180
|
||||
}
|
||||
} satisfies ExplorerSettings<TOrder>);
|
||||
}) satisfies ExplorerSettings<TOrder>;
|
||||
|
||||
type CutCopyState =
|
||||
| {
|
||||
|
|
|
@ -43,8 +43,13 @@ const JobContainer = forwardRef<HTMLLIElement, JobContainerProps>((props, ref) =
|
|||
)
|
||||
)}
|
||||
<MetaContainer>
|
||||
<Tooltip asChild tooltipClassName="bg-black max-w-[400px]" position="top" label={name}>
|
||||
<p className="truncate w-fit max-w-[83%] pl-1.5 font-semibold">{name}</p>
|
||||
<Tooltip
|
||||
asChild
|
||||
tooltipClassName="bg-black max-w-[400px]"
|
||||
position="top"
|
||||
label={name}
|
||||
>
|
||||
<p className="w-fit max-w-[83%] truncate pl-1.5 font-semibold">{name}</p>
|
||||
</Tooltip>
|
||||
{textItems?.map((item, index) => {
|
||||
// filter out undefined text so we don't render empty TextItems
|
||||
|
|
|
@ -14,8 +14,8 @@ export const Component = () => {
|
|||
|
||||
const filteredLocations = useMemo(
|
||||
() =>
|
||||
locations.data?.filter((location) =>
|
||||
location.name?.toLowerCase().includes(debouncedSearch.toLowerCase())
|
||||
locations.data?.filter(
|
||||
(location) => location.name?.toLowerCase().includes(debouncedSearch.toLowerCase())
|
||||
),
|
||||
[debouncedSearch, locations.data]
|
||||
);
|
||||
|
|
|
@ -36,9 +36,7 @@ export const Component = () => {
|
|||
|
||||
return (
|
||||
<ul className="space-y-4 p-4">
|
||||
{groups?.map((group, index) => (
|
||||
<OperationGroup key={index} group={group} />
|
||||
))}
|
||||
{groups?.map((group, index) => <OperationGroup key={index} group={group} />)}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,7 +23,7 @@ const Accordion = ({ title, className, children }: Props) => {
|
|||
/>
|
||||
</div>
|
||||
{toggle && (
|
||||
<div className="p-3 pt-2 border-t rounded-b-md border-app-line bg-app-box">
|
||||
<div className="rounded-b-md border-t border-app-line bg-app-box p-3 pt-2">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -24,7 +24,12 @@ prettier.resolveConfig(join(__dirname, '..', '..', '..', '.prettierrc.js')).then
|
|||
const indexFilePath = join(__dirname, '..', folder, 'index.ts');
|
||||
const assetsFolderPath = join(__dirname, '..', folder);
|
||||
|
||||
if (await fs.access(indexFilePath).then(() => true, () => false)) {
|
||||
if (
|
||||
await fs.access(indexFilePath).then(
|
||||
() => true,
|
||||
() => false
|
||||
)
|
||||
) {
|
||||
// Delete the index file if it already exists.
|
||||
await fs.unlink(indexFilePath);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ export type Procedures = {
|
|||
{ key: "buildInfo", input: never, result: BuildInfo } |
|
||||
{ key: "categories.list", input: LibraryArgs<null>, result: { [key in Category]: number } } |
|
||||
{ key: "files.get", input: LibraryArgs<GetArgs>, result: { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; file_paths: FilePath[] } | null } |
|
||||
{ key: "files.getEphemeralMediaData", input: string, result: MediaMetadata | null } |
|
||||
{ key: "files.getMediaData", input: LibraryArgs<number>, result: MediaMetadata } |
|
||||
{ key: "files.getPath", input: LibraryArgs<number>, result: string | null } |
|
||||
{ key: "invalidation.test-invalidate", input: never, result: number } |
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
import { comlink } from 'vite-plugin-comlink';
|
||||
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||
import svg from 'vite-plugin-svgr';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { comlink } from 'vite-plugin-comlink';
|
||||
import relativeAliasResolver from './relativeAliasResolver';
|
||||
|
||||
export default defineConfig({
|
||||
|
|
|
@ -29,14 +29,13 @@ const itemIconStyles = cva('mr-2 h-4 w-4', {
|
|||
variants: {}
|
||||
});
|
||||
|
||||
type DropdownItemProps =
|
||||
| PropsWithChildren<{
|
||||
to?: string;
|
||||
className?: string;
|
||||
icon?: any;
|
||||
onClick?: () => void;
|
||||
}> &
|
||||
VariantProps<typeof itemStyles>;
|
||||
type DropdownItemProps = PropsWithChildren<{
|
||||
to?: string;
|
||||
className?: string;
|
||||
icon?: any;
|
||||
onClick?: () => void;
|
||||
}> &
|
||||
VariantProps<typeof itemStyles>;
|
||||
|
||||
export const Item = ({ to, className, icon: Icon, children, ...props }: DropdownItemProps) => {
|
||||
const content = (
|
||||
|
|
Loading…
Reference in a new issue