Use search.objects for recents query (#850)

* separate search + filter + ordering

* typesafe categories

* make ordering great again

* fix ts

* eslint
This commit is contained in:
Brendan Allan
2023-05-23 20:58:45 +08:00
committed by GitHub
parent 9fb72a1dc8
commit da2e78dc61
27 changed files with 552 additions and 359 deletions

BIN
Cargo.lock generated
View File

Binary file not shown.

View File

@@ -37,6 +37,7 @@ prisma-client-rust-sdk = { git = "https://github.com/Brendonovich/prisma-client-
rspc = { version = "0.1.4" }
specta = { version = "1.0.4" }
httpz = { version = "0.0.3" }
tauri-specta = { version = "1.0.2" }
swift-rs = { version = "1.0.5" }
@@ -50,7 +51,9 @@ if-watch = { git = "https://github.com/oscartbeaumont/if-watch", rev = "410e8e1d
mdns-sd = { git = "https://github.com/oscartbeaumont/mdns-sd", rev = "45515a98e9e408c102871abaa5a9bff3bee0cbe8" } # TODO: Do upstream PR
rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "799eec5df7533edf331f41d3f1be03de07e038d7" }
httpz = { git = "https://github.com/oscartbeaumont/httpz", rev = "a5185f2ed2fdefeb2f582dce38a692a1bf76d1d6" }
specta = { git = "https://github.com/oscartbeaumont/specta", branch = "v2" }
rspc = { git = "https://github.com/oscartbeaumont/rspc", branch = "specta-v2" }
tauri-specta = { git = "https://github.com/oscartbeaumont/tauri-specta", branch = "specta-v2" }
swift-rs = { git = "https://github.com/Brendonovich/swift-rs", rev = "973c22215734d1d5b97c496601d658371e537ece" }

View File

@@ -28,7 +28,7 @@ percent-encoding = "2.2.0"
http = "0.2.8"
opener = "0.6.1"
specta.workspace = true
tauri-specta = { version = "1.0.0", features = ["typescript"] }
tauri-specta = { workspace = true, features = ["typescript"] }
uuid = { version = "1.1.2", features = ["serde"] }
[target.'cfg(target_os = "linux")'.dependencies]

View File

@@ -1,3 +1,4 @@
/* eslint-disable */
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
declare global {
@@ -6,22 +7,23 @@ declare global {
}
}
const invoke = window.__TAURI_INVOKE__;
// Function avoids 'window not defined' in SSR
const invoke = () => window.__TAURI_INVOKE__;
export function appReady() {
return invoke<null>("app_ready")
return invoke()<null>("app_ready")
}
export function openFilePath(library: string, id: number) {
return invoke<OpenFilePathResult>("open_file_path", { library,id })
return invoke()<OpenFilePathResult>("open_file_path", { library,id })
}
export function getFilePathOpenWithApps(library: string, id: number) {
return invoke<OpenWithApplication[]>("get_file_path_open_with_apps", { library,id })
return invoke()<OpenWithApplication[]>("get_file_path_open_with_apps", { library,id })
}
export function openFilePathWith(library: string, id: number, withUrl: string) {
return invoke<null>("open_file_path_with", { library,id,withUrl })
return invoke()<null>("open_file_path_with", { library,id,withUrl })
}
export type OpenWithApplication = { name: string; url: string }

View File

@@ -12,8 +12,10 @@ export default function LocationScreen({ navigation, route }: SharedScreenProps<
const { data } = useLibraryQuery([
'search.paths',
{
locationId: id,
path: path ?? ''
filter: {
locationId: id,
path: path ?? ''
}
}
]);

View File

@@ -9,7 +9,9 @@ export default function TagScreen({ navigation, route }: SharedScreenProps<'Tag'
const search = useLibraryQuery([
'search.objects',
{
tagId: id
filter: {
tags: [id]
}
}
]);

View File

@@ -1,36 +1,25 @@
use crate::library::{get_category_count, Category};
use std::str::FromStr;
use std::{collections::BTreeMap, str::FromStr};
use rspc::alpha::AlphaRouter;
use serde::{Deserialize, Serialize};
use specta::Type;
use strum::VariantNames;
use super::{utils::library, Ctx, R};
pub(crate) fn mount() -> AlphaRouter<Ctx> {
R.router().procedure("list", {
#[derive(Type, Deserialize, Serialize)]
struct CategoryItem {
name: String,
count: i32,
}
R.with2(library()).query(|(_, library), _: ()| async move {
let mut category_items = Vec::with_capacity(Category::VARIANTS.len());
let mut data = BTreeMap::new();
for category_str in Category::VARIANTS {
let category = Category::from_str(category_str)
.expect("it's alright this category string exists");
// Convert the category to a CategoryItem and push to vector.
category_items.push(CategoryItem {
name: category_str.to_string(),
count: get_category_count(&library.db, category).await,
});
data.insert(category, get_category_count(&library.db, category).await);
}
Ok(category_items)
Ok(data)
})
})
}

View File

@@ -6,7 +6,7 @@ use crate::{
copy::FileCopierJobInit, cut::FileCutterJobInit, decrypt::FileDecryptorJobInit,
delete::FileDeleterJobInit, encrypt::FileEncryptorJobInit, erase::FileEraserJobInit,
},
prisma::{file_path, location, object, SortOrder},
prisma::{location, object},
};
use chrono::{FixedOffset, Utc};
@@ -16,10 +16,7 @@ use specta::Type;
use std::path::Path;
use tokio::fs;
use super::{
locations::{file_path_with_object, ExplorerItem},
Ctx, R,
};
use super::{Ctx, R};
pub(crate) fn mount() -> AlphaRouter<Ctx> {
R.router()

View File

@@ -1,12 +1,11 @@
use crate::location::file_path_helper::{check_file_path_exists, IsolatedFilePathData};
use std::collections::BTreeSet;
use chrono::{DateTime, Utc};
use chrono::{DateTime, FixedOffset, Utc};
use prisma_client_rust::operator::or;
use rspc::{alpha::AlphaRouter, ErrorCode};
use serde::{Deserialize, Serialize};
use specta::Type;
use uuid::Uuid;
use crate::{
api::{
@@ -15,7 +14,7 @@ use crate::{
},
library::Library,
location::{find_location, LocationError},
prisma::*,
prisma::{self, file_path, object, tag, tag_on_object},
util::db::chain_optional_iter,
};
@@ -27,36 +26,50 @@ struct SearchData<T> {
items: Vec<T>,
}
#[derive(Deserialize, Default, Type)]
#[derive(Deserialize, Default, Type, Debug)]
#[serde(rename_all = "camelCase")]
struct OptionalRange<T> {
from: Option<T>,
to: Option<T>,
}
#[derive(Deserialize, Type, Debug, Clone, Copy)]
enum SortOrder {
Asc,
Desc,
}
impl Into<prisma::SortOrder> for SortOrder {
fn into(self) -> prisma::SortOrder {
match self {
Self::Asc => prisma::SortOrder::Asc,
Self::Desc => prisma::SortOrder::Desc,
}
}
}
#[derive(Deserialize, Type, Debug, Clone)]
#[serde(rename_all = "camelCase")]
enum FilePathSearchOrdering {
Name(bool),
SizeInBytes(bool),
DateCreated(bool),
DateModified(bool),
DateIndexed(bool),
Name(SortOrder),
SizeInBytes(SortOrder),
DateCreated(SortOrder),
DateModified(SortOrder),
DateIndexed(SortOrder),
Object(Box<ObjectSearchOrdering>),
}
impl FilePathSearchOrdering {
fn get_sort_order(&self) -> SortOrder {
match self {
fn get_sort_order(&self) -> prisma::SortOrder {
(*match self {
Self::Name(v) => v,
Self::SizeInBytes(v) => v,
Self::DateCreated(v) => v,
Self::DateModified(v) => v,
Self::DateIndexed(v) => v,
Self::Object(v) => return v.get_sort_order(),
}
.then_some(SortOrder::Asc)
.unwrap_or(SortOrder::Desc)
})
.into()
}
fn to_param(self) -> file_path::OrderByWithRelationParam {
@@ -73,50 +86,64 @@ impl FilePathSearchOrdering {
}
}
#[derive(Deserialize, Type)]
#[derive(Deserialize, Type, Debug)]
#[serde(untagged)]
enum MaybeNot<T> {
None(T),
Not { not: T },
}
impl<T> MaybeNot<T> {
fn to_prisma<R: From<prisma_client_rust::Operator<R>>>(self, param: fn(T) -> R) -> R {
match self {
Self::None(v) => param(v),
Self::Not { not } => prisma_client_rust::not![param(not)],
}
}
}
#[derive(Deserialize, Type, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FilePathSearchArgs {
struct FilePathFilterArgs {
#[specta(optional)]
location_id: Option<i32>,
#[specta(optional)]
after_file_id: Option<Uuid>,
#[specta(optional)]
take: Option<i32>,
#[specta(optional)]
order: Option<FilePathSearchOrdering>,
#[serde(default)]
search: String,
#[specta(optional)]
extension: Option<String>,
#[serde(default)]
kind: BTreeSet<i32>,
#[serde(default)]
tags: Vec<i32>,
#[serde(default)]
created_at: OptionalRange<DateTime<Utc>>,
#[specta(optional)]
path: Option<String>,
#[specta(optional)]
object: Option<ObjectFilterArgs>,
}
#[derive(Deserialize, Type, Debug)]
#[serde(rename_all = "camelCase")]
struct FilePathSearchArgs {
#[specta(optional)]
take: Option<i32>,
#[specta(optional)]
order: Option<FilePathSearchOrdering>,
#[specta(optional)]
cursor: Option<Vec<u8>>,
#[specta(optional)]
favorite: Option<bool>,
#[specta(optional)]
hidden: Option<bool>,
#[serde(default)]
filter: FilePathFilterArgs,
}
#[derive(Deserialize, Type, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub enum ObjectSearchOrdering {
DateAccessed(bool),
enum ObjectSearchOrdering {
DateAccessed(SortOrder),
}
impl ObjectSearchOrdering {
fn get_sort_order(&self) -> SortOrder {
match self {
fn get_sort_order(&self) -> prisma::SortOrder {
(*match self {
Self::DateAccessed(v) => v,
}
.then_some(SortOrder::Asc)
.unwrap_or(SortOrder::Desc)
})
.into()
}
fn to_param(self) -> object::OrderByWithRelationParam {
@@ -128,26 +155,70 @@ impl ObjectSearchOrdering {
}
}
#[derive(Deserialize, Type, Debug, Default)]
#[serde(rename_all = "camelCase")]
struct ObjectFilterArgs {
#[specta(optional)]
favorite: Option<bool>,
#[specta(optional)]
hidden: Option<bool>,
#[specta(optional)]
date_accessed: Option<MaybeNot<Option<chrono::DateTime<FixedOffset>>>>,
#[serde(default)]
kind: BTreeSet<i32>,
#[serde(default)]
tags: Vec<i32>,
}
impl ObjectFilterArgs {
fn to_params(self) -> Vec<object::WhereParam> {
chain_optional_iter(
[],
[
self.favorite.map(object::favorite::equals),
self.hidden.map(object::hidden::equals),
self.date_accessed
.map(|date| date.to_prisma(object::date_accessed::equals)),
(!self.kind.is_empty())
.then(|| object::kind::in_vec(self.kind.into_iter().collect())),
(!self.tags.is_empty()).then(|| {
let tags = self.tags.into_iter().map(tag::id::equals).collect();
let tags_on_object = tag_on_object::tag::is(vec![or(tags)]);
object::tags::some(vec![tags_on_object])
}),
],
)
}
}
#[derive(Deserialize, Type, Debug)]
#[serde(rename_all = "camelCase")]
struct ObjectSearchArgs {
#[specta(optional)]
take: Option<i32>,
#[serde(default)]
#[specta(optional)]
tag_id: Option<i32>,
order: Option<ObjectSearchOrdering>,
#[specta(optional)]
cursor: Option<Vec<u8>>,
#[serde(default)]
filter: ObjectFilterArgs,
}
pub fn mount() -> AlphaRouter<Ctx> {
R.router()
.procedure("paths", {
R.with2(library())
.query(|(_, library), args: FilePathSearchArgs| async move {
R.with2(library()).query(
|(_, library),
FilePathSearchArgs {
take,
order,
cursor,
filter,
}| async move {
let Library { db, .. } = &library;
let location = if let Some(location_id) = args.location_id {
let location = if let Some(location_id) = filter.location_id {
Some(
find_location(&library, location_id)
.exec()
@@ -158,7 +229,7 @@ pub fn mount() -> AlphaRouter<Ctx> {
None
};
let directory_materialized_path_str = match (args.path, location) {
let directory_materialized_path_str = match (filter.path, location) {
(Some(path), Some(location)) if !path.is_empty() && path != "/" => {
let parent_iso_file_path =
IsolatedFilePathData::from_relative_str(location.id, &path);
@@ -177,56 +248,42 @@ pub fn mount() -> AlphaRouter<Ctx> {
_ => None,
};
let object_params = chain_optional_iter(
[],
[
args.favorite.map(object::favorite::equals),
args.hidden.map(object::hidden::equals),
(!args.kind.is_empty())
.then(|| object::kind::in_vec(args.kind.into_iter().collect())),
(!args.tags.is_empty()).then(|| {
let tags = args.tags.into_iter().map(tag::id::equals).collect();
let tags_on_object = tag_on_object::tag::is(vec![or(tags)]);
object::tags::some(vec![tags_on_object])
}),
],
);
let params = chain_optional_iter(
args.search
filter
.search
.split(' ')
.map(str::to_string)
.map(file_path::name::contains),
[
args.location_id.map(file_path::location_id::equals),
args.extension.map(file_path::extension::equals),
args.created_at
filter.location_id.map(file_path::location_id::equals),
filter.extension.map(file_path::extension::equals),
filter
.created_at
.from
.map(|v| file_path::date_created::gte(v.into())),
args.created_at
filter
.created_at
.to
.map(|v| file_path::date_created::lte(v.into())),
directory_materialized_path_str
.map(file_path::materialized_path::equals),
(!object_params.is_empty())
.then(|| file_path::object::is(object_params)),
filter.object.and_then(|obj| {
let params = obj.to_params();
(!params.is_empty()).then(|| file_path::object::is(params))
}),
],
);
let take = args.take.unwrap_or(100);
let take = take.unwrap_or(100);
let mut query = db.file_path().find_many(params).take(take as i64 + 1);
if let Some(file_id) = args.after_file_id {
query = query.cursor(file_path::pub_id::equals(file_id.as_bytes().to_vec()))
}
if let Some(order) = args.order {
if let Some(order) = order {
query = query.order_by(order.to_param());
}
if let Some(cursor) = args.cursor {
if let Some(cursor) = cursor {
query = query.cursor(file_path::pub_id::equals(cursor));
}
@@ -263,26 +320,32 @@ pub fn mount() -> AlphaRouter<Ctx> {
}
Ok(SearchData { items, cursor })
})
},
)
})
.procedure("objects", {
R.with2(library())
.query(|(_, library), args: ObjectSearchArgs| async move {
R.with2(library()).query(
|(_, library),
ObjectSearchArgs {
take,
order,
cursor,
filter,
}| async move {
let Library { db, .. } = &library;
let take = args.take.unwrap_or(100);
let take = take.unwrap_or(100);
let mut query = db
.object()
.find_many(chain_optional_iter(
[],
[args.tag_id.map(|id| {
object::tags::some(vec![tag_on_object::tag_id::equals(id)])
})],
))
.find_many(filter.to_params())
.take(take as i64 + 1);
if let Some(cursor) = args.cursor {
if let Some(order) = order {
query = query.order_by(order.to_param());
}
if let Some(cursor) = cursor {
query = query.cursor(object::pub_id::equals(cursor));
}
@@ -328,6 +391,7 @@ pub fn mount() -> AlphaRouter<Ctx> {
}
Ok(SearchData { items, cursor })
})
},
)
})
}

View File

@@ -8,8 +8,20 @@ use std::{sync::Arc, vec};
use strum_macros::{EnumString, EnumVariantNames};
/// Meow
#[derive(Serialize, Deserialize, Type, Debug, EnumVariantNames, EnumString)]
#[serde(tag = "type")]
#[derive(
Serialize,
Deserialize,
Type,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
EnumVariantNames,
EnumString,
Clone,
Copy,
)]
pub enum Category {
Recents,
Favorites,
@@ -17,17 +29,16 @@ pub enum Category {
Videos,
Movies,
Music,
// Documents,
Documents,
Downloads,
Encrypted,
Projects,
// Applications,
// Archives,
// Databases
Applications,
Archives,
Databases,
Games,
Books,
// Contacts,
// Movies,
Contacts,
Trash,
}
@@ -45,35 +56,16 @@ impl Category {
}
pub async fn get_category_count(db: &Arc<PrismaClient>, category: Category) -> i32 {
let params = match category {
Category::Recents => vec![not![object::date_accessed::equals(None)]],
Category::Favorites => vec![object::favorite::equals(true)],
let param = match category {
Category::Recents => not![object::date_accessed::equals(None)],
Category::Favorites => object::favorite::equals(true),
Category::Photos
| Category::Videos
| Category::Music
| Category::Encrypted
| Category::Books => vec![object::kind::equals(category.to_object_kind() as i32)],
Category::Downloads => {
// TODO: Fetch the actual count for the Downloads category.
return 0;
}
Category::Projects => {
// TODO: Fetch the actual count for the Projects category.
return 0;
}
Category::Games => {
// TODO: Fetch the actual count for the Games category.
return 0;
}
Category::Movies => {
// TODO: Fetch the actual count for the Trash category.
return 0;
}
Category::Trash => {
// TODO: Fetch the actual count for the Trash category.
return 0;
}
| Category::Books => object::kind::equals(category.to_object_kind() as i32),
_ => return 0,
};
db.object().count(params).exec().await.unwrap_or(0) as i32
db.object().count(vec![param]).exec().await.unwrap_or(0) as i32
}

View File

@@ -32,7 +32,7 @@ const MediaViewItem = memo(({ data, index }: MediaViewItemProps) => {
>
<div
className={clsx(
'hover:bg-app-selectedItem group relative flex aspect-square items-center justify-center',
'group relative flex aspect-square items-center justify-center hover:bg-app-selectedItem',
selected && 'bg-app-selectedItem'
)}
>

View File

@@ -1,9 +1,10 @@
import { RadixCheckbox, Select, SelectOption, Slider, tw } from '@sd/ui';
import { z } from 'zod';
import {
ExplorerDirection,
ExplorerOrderByKeys,
FilePathSearchOrderingKeys,
getExplorerConfigStore,
getExplorerStore,
SortOrder,
useExplorerConfigStore,
useExplorerStore
} from '~/hooks';
@@ -11,14 +12,14 @@ import {
const Heading = tw.div`text-ink-dull text-xs font-semibold`;
const Subheading = tw.div`text-ink-dull mb-1 text-xs font-medium`;
const sortOptions: Record<ExplorerOrderByKeys, string> = {
const sortOptions: Record<FilePathSearchOrderingKeys, string> = {
none: 'None',
name: 'Name',
sizeInBytes: 'Size',
dateCreated: 'Date created',
dateModified: 'Date modified',
dateIndexed: 'Date indexed',
object: 'Object'
"object.dateAccessed": "Date accessed"
};
export default () => {
@@ -60,7 +61,7 @@ export default () => {
size="sm"
className="w-full"
onChange={(value) =>
(getExplorerStore().orderBy = value as ExplorerOrderByKeys)
(getExplorerStore().orderBy = value as FilePathSearchOrderingKeys)
}
>
{Object.entries(sortOptions).map(([value, text]) => (
@@ -77,11 +78,12 @@ export default () => {
size="sm"
className="w-full"
onChange={(value) =>
(getExplorerStore().orderByDirection = value as ExplorerDirection)
(getExplorerStore().orderByDirection = value as z.infer<typeof SortOrder>)
}
>
<SelectOption value="asc">Asc</SelectOption>
<SelectOption value="desc">Desc</SelectOption>
{SortOrder.options.map(o => (
<SelectOption key={o.value} value={o.value}>{o.value}</SelectOption>
))}
</Select>
</div>
</div>

View File

@@ -8,15 +8,27 @@ import {
isPath
} from '@sd/client';
import { useExplorerStore, useZodSearchParams } from '~/hooks';
import { useMemo } from 'react';
export function useExplorerOrder(): FilePathSearchOrdering | undefined {
const explorerStore = useExplorerStore();
if (explorerStore.orderBy === 'none') return undefined;
const ordering = useMemo(() => {
if (explorerStore.orderBy === 'none') return undefined;
return {
[explorerStore.orderBy]: explorerStore.orderByDirection === 'asc'
} as FilePathSearchOrdering;
const obj = {};
explorerStore.orderBy.split('.').reduce((acc, next, i, all) => {
if(all.length - 1 === i) acc[next] = explorerStore.orderByDirection;
else acc[next] = {}
return acc[next]
}, obj as any)
return obj as FilePathSearchOrdering;
}, [explorerStore.orderBy, explorerStore.orderByDirection])
return ordering
}
export function getItemObject(data: ExplorerItem) {

View File

@@ -8,9 +8,8 @@ import {
useLibraryQuery
} from '@sd/client';
import { Button, SelectOption, forms } from '@sd/ui';
import { Form } from '~/../packages/ui/src/forms';
const { z, useZodForm, PasswordInput, Select } = forms;
const { z, useZodForm, PasswordInput, Select, Form } = forms;
const schema = z
.object({

View File

@@ -148,10 +148,10 @@ function Job({ job, clearJob, className, isGroup }: JobProps) {
)}
/>
</div>
<div className="flex flex-col w-full">
<div className="flex w-full flex-col">
<div className="flex items-center">
<div className="truncate">
<span className="font-semibold truncate">{niceData.name}</span>
<span className="truncate font-semibold">{niceData.name}</span>
<p className="mb-[5px] mt-[2px] flex gap-1 truncate text-ink-faint">
{job.status === 'Queued' && <p>{job.status}:</p>}
{niceData.filesDiscovered}
@@ -161,7 +161,7 @@ function Job({ job, clearJob, className, isGroup }: JobProps) {
<div className="flex gap-1 truncate text-ink-faint"></div>
</div>
<div className="grow" />
<div className="flex flex-row space-x-2 ml-7">
<div className="ml-7 flex flex-row space-x-2">
{/* {job.status === 'Running' && (
<Button size="icon">
<Tooltip label="Coming Soon">

View File

@@ -4,17 +4,11 @@ import { useLocation, useNavigate, useResolvedPath } from 'react-router';
import { createSearchParams } from 'react-router-dom';
import { useKey, useKeys } from 'rooks';
import { useDebouncedCallback } from 'use-debounce';
import { z } from 'zod';
import { Input, Shortcut } from '@sd/ui';
import { useZodSearchParams } from '~/hooks';
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
import { getSearchStore } from '~/hooks/useSearchStore';
export const SEARCH_PARAM_KEY = 'search';
export const SEARCH_PARAMS = z.object({
search: z.string().default('')
});
import { SEARCH_PARAMS } from '../search';
export default () => {
const searchRef = useRef<HTMLInputElement>(null);
@@ -33,7 +27,7 @@ export default () => {
const searchPath = useResolvedPath('search');
const [value, setValue] = useState(searchParams.search);
const [value, setValue] = useState(searchParams.search ?? "");
const updateParams = useDebouncedCallback((value: string) => {
startTransition(() =>

View File

@@ -31,7 +31,7 @@ export const Component = () => {
const { explorerViewOptions, explorerControlOptions, explorerToolOptions } =
useExplorerTopBarOptions();
const { data: location } = useLibraryQuery(['locations.get', location_id]);
const location = useLibraryQuery(['locations.get', location_id]);
// we destructure this since `mutate` is a stable reference but the object it's in is not
const { mutate: quickRescan } = useLibraryMutation('locations.quickRescan');
@@ -54,7 +54,7 @@ export const Component = () => {
<>
<Folder size={22} className="-mt-[1px] ml-3 mr-2 inline-block" />
<span className="text-sm font-medium">
{path ? getLastSectionOfPath(path) : location?.name}
{path ? getLastSectionOfPath(path) : location.data?.name}
</span>
</>
}
@@ -64,7 +64,7 @@ export const Component = () => {
/>
}
/>
<div className="relative flex flex-col w-full">
<div className="relative flex w-full flex-col">
<Explorer
items={items}
onLoadMore={query.fetchNextPage}
@@ -92,11 +92,13 @@ const useItems = () => {
library_id: library.uuid,
arg: {
order: useExplorerOrder(),
locationId,
filter: {
locationId,
...(explorerState.layoutMode === 'media'
? { object: { kind: [5, 7] } }
: { path: path ?? '' })
},
take,
...(explorerState.layoutMode === 'media'
? { kind: [5, 7] }
: { path: path ?? '' })
}
}
] as const,

View File

@@ -0,0 +1,50 @@
import { getIcon } from '@sd/assets/util';
import { Category, useLibraryQuery } from '@sd/client';
import { useIsDark } from '~/hooks';
import CategoryButton from './CategoryButton';
import { IconForCategory } from './data';
const CategoryList = [
'Recents',
'Favorites',
'Photos',
'Videos',
'Movies',
'Music',
'Documents',
'Downloads',
'Encrypted',
'Projects',
'Applications',
'Archives',
'Databases',
'Games',
'Books',
'Contacts',
'Trash'
] as Category[];
export const Categories = (props: { selected: Category; onSelectedChanged(c: Category): void }) => {
const categories = useLibraryQuery(['categories.list']);
const isDark = useIsDark();
return (
<div className="no-scrollbar sticky top-0 z-10 mt-2 flex space-x-[1px] overflow-x-scroll bg-app/90 px-5 py-1.5 backdrop-blur">
{categories.data &&
CategoryList.map((category) => {
const iconString = IconForCategory[category] || 'Document';
return (
<CategoryButton
key={category}
category={category}
icon={getIcon(iconString, isDark)}
items={categories.data[category]}
selected={props.selected === category}
onClick={() => props.onSelectedChanged(category)}
/>
);
})}
</div>
);
};

View File

@@ -1,6 +1,7 @@
import byteSize from 'byte-size';
import clsx from 'clsx';
import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';
import { Statistics, useLibraryContext, useLibraryQuery } from '@sd/client';
import { useCounter } from '~/hooks';
import { usePlatform } from '~/util/Platform';

View File

@@ -0,0 +1,163 @@
import { iconNames } from '@sd/assets/util';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import deepMerge from 'ts-deepmerge';
import {
Category,
FilePathSearchArgs,
ObjectKind,
ObjectKindKey,
ObjectSearchArgs,
useLibraryContext,
useRspcLibraryContext
} from '@sd/client';
import { useExplorerStore } from '~/hooks';
export const IconForCategory: Partial<Record<Category, string>> = {
Recents: iconNames.Collection,
Favorites: iconNames.HeartFlat,
Photos: iconNames.Image,
Videos: iconNames.Video,
Movies: iconNames.Movie,
Music: iconNames.Audio,
Documents: iconNames.Document,
Downloads: iconNames.Package,
Applications: iconNames.Application,
Games: iconNames.Game,
Books: iconNames.Book,
Encrypted: iconNames.EncryptedLock,
Archives: iconNames.Database,
Projects: iconNames.Folder,
Trash: iconNames.Trash
};
// Map the category to the ObjectKind for searching
const SearchableCategories: Record<string, ObjectKindKey> = {
Photos: 'Image',
Videos: 'Video',
Music: 'Audio',
Documents: 'Document',
Encrypted: 'Encrypted',
Books: 'Book'
} satisfies Partial<Record<Category, ObjectKindKey>>;
const OBJECT_CATEGORIES: Category[] = ['Recents', 'Favorites'];
// this is a gross function so it's in a separate hook :)
export function useItems(selectedCategory: Category) {
const explorerStore = useExplorerStore();
const ctx = useRspcLibraryContext();
const { library } = useLibraryContext();
const searchableCategory = SearchableCategories[selectedCategory];
const searchableCategoryKind =
searchableCategory !== undefined ? (ObjectKind[searchableCategory] as number) : undefined;
const kind = searchableCategoryKind ? [searchableCategoryKind] : undefined;
if (explorerStore.layoutMode === 'media') [5, 7].forEach((v) => kind?.push(v));
const isObjectQuery = OBJECT_CATEGORIES.includes(selectedCategory);
// TODO: Make a custom double click handler for directories to take users to the location explorer.
// For now it's not needed because folders shouldn't show.
const pathsQuery = useInfiniteQuery({
enabled: !isObjectQuery,
queryKey: [
'search.paths',
{
library_id: library.uuid,
arg: deepMerge(
{
take: 50,
filter: {
object: { kind }
}
},
categorySearchPathsArgs(selectedCategory)
)
}
] as const,
queryFn: ({ pageParam: cursor, queryKey }) =>
ctx.client.query([
'search.paths',
{
...queryKey[1].arg,
cursor
}
]),
getNextPageParam: (lastPage) => lastPage.cursor ?? undefined
});
const pathsItems = useMemo(
() => pathsQuery.data?.pages?.flatMap((d) => d.items),
[pathsQuery.data]
);
const objectsQuery = useInfiniteQuery({
enabled: isObjectQuery,
queryKey: [
'search.objects',
{
library_id: library.uuid,
arg: deepMerge(
{
take: 50,
filter: {
kind
}
},
categorySearchObjectsArgs(selectedCategory)
)
}
] as const,
queryFn: ({ pageParam: cursor, queryKey }) =>
ctx.client.query([
'search.objects',
{
...queryKey[1].arg,
cursor
}
]),
getNextPageParam: (lastPage) => lastPage.cursor ?? undefined
});
const objectsItems = useMemo(
() => objectsQuery.data?.pages?.flatMap((d) => d.items),
[objectsQuery.data]
);
return isObjectQuery
? {
items: objectsItems,
query: objectsQuery
}
: {
items: pathsItems,
query: pathsQuery
};
}
function categorySearchPathsArgs(_: string): FilePathSearchArgs {
return {};
}
function categorySearchObjectsArgs(category: string): ObjectSearchArgs {
if (category === 'Recents')
return {
order: { dateAccessed: 'Desc' },
filter: {
dateAccessed: {
not: null
}
}
};
if (category === 'Favorites')
return {
filter: {
favorite: true
}
};
return {};
}

View File

@@ -2,129 +2,28 @@ import { getIcon, iconNames } from '@sd/assets/util';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import 'react-loading-skeleton/dist/skeleton.css';
import {
ExplorerItem,
ObjectKind,
ObjectKindKey,
useLibraryContext,
useLibraryQuery,
useRspcLibraryContext
} from '@sd/client';
import { z } from '@sd/ui/src/forms';
import { useExplorerStore, useExplorerTopBarOptions, useIsDark } from '~/hooks';
import { useExplorerTopBarOptions } from '~/hooks';
import Explorer from '../Explorer';
import { SEARCH_PARAMS, useExplorerOrder } from '../Explorer/util';
import { SEARCH_PARAMS } from '../Explorer/util';
import { usePageLayout } from '../PageLayout';
import { TopBarPortal } from '../TopBar/Portal';
import TopBarOptions from '../TopBar/TopBarOptions';
import CategoryButton from '../overview/CategoryButton';
import Statistics from '../overview/Statistics';
// TODO: Replace left hand type with Category enum type (doesn't exist yet)
const CategoryToIcon: Record<string, string> = {
Recents: iconNames.Collection,
Favorites: iconNames.HeartFlat,
Photos: iconNames.Image,
Videos: iconNames.Video,
Movies: iconNames.Movie,
Music: iconNames.Audio,
Documents: iconNames.Document,
Downloads: iconNames.Package,
Applications: iconNames.Application,
Games: iconNames.Game,
Books: iconNames.Book,
Encrypted: iconNames.EncryptedLock,
Archives: iconNames.Database,
Projects: iconNames.Folder,
Trash: iconNames.Trash
};
// Map the category to the ObjectKind for searching
const SearchableCategories: Record<string, ObjectKindKey> = {
Photos: 'Image',
Videos: 'Video',
Music: 'Audio',
Documents: 'Document',
Encrypted: 'Encrypted',
Books: 'Book'
};
import { Categories } from './Categories';
import { useItems } from "./data"
import { Category } from '~/../packages/client/src';
export type SearchArgs = z.infer<typeof SEARCH_PARAMS>;
export const Component = () => {
const page = usePageLayout();
const isDark = useIsDark();
const explorerStore = useExplorerStore();
const ctx = useRspcLibraryContext();
const { library } = useLibraryContext();
const { explorerViewOptions, explorerControlOptions, explorerToolOptions } =
useExplorerTopBarOptions();
const [selectedCategory, setSelectedCategory] = useState<string>('Recents');
const [selectedCategory, setSelectedCategory] = useState<Category>('Recents');
// TODO: integrate this into search query
const recentFiles = useLibraryQuery([
'search.paths',
{
order: { object: { dateAccessed: false } },
take: 50
}
]);
// this should be redundant once above todo is complete
const canSearch = !!SearchableCategories[selectedCategory] || selectedCategory === 'Favorites';
const kind = ObjectKind[SearchableCategories[selectedCategory] || 0] as number;
const categories = useLibraryQuery(['categories.list']);
const isFavoritesCategory = selectedCategory === 'Favorites';
// TODO: Make a custom double click handler for directories to take users to the location explorer.
// For now it's not needed because folders shouldn't show.
const query = useInfiniteQuery({
enabled: canSearch,
queryKey: [
'search.paths',
{
library_id: library.uuid,
arg: {
order: useExplorerOrder(),
favorite: isFavoritesCategory ? true : undefined,
...(explorerStore.layoutMode === 'media'
? {
kind: [5, 7].includes(kind)
? [kind]
: isFavoritesCategory
? [5, 7]
: [5, 7, kind]
}
: { kind: isFavoritesCategory ? [] : [kind] })
}
}
] as const,
queryFn: ({ pageParam: cursor, queryKey }) =>
ctx.client.query([
'search.paths',
{
...queryKey[1].arg,
cursor
}
]),
getNextPageParam: (lastPage) => lastPage.cursor ?? undefined
});
const searchItems = useMemo(() => query.data?.pages?.flatMap((d) => d.items), [query.data]);
let items: ExplorerItem[] = [];
switch (selectedCategory) {
case 'Recents':
items = recentFiles.data?.items || [];
break;
default:
if (canSearch) {
items = searchItems || [];
}
}
const { items, query } = useItems(selectedCategory);
return (
<>
@@ -147,23 +46,8 @@ export const Component = () => {
isFetchingNextPage={query.isFetchingNextPage}
scrollRef={page?.ref}
>
<div className="no-scrollbar sticky top-0 z-10 mt-2 flex space-x-[1px] overflow-x-scroll bg-app/90 px-5 py-1.5 backdrop-blur">
{categories.data?.map((category) => {
const iconString = CategoryToIcon[category.name] || 'Document';
return (
<CategoryButton
key={category.name}
category={category.name}
icon={getIcon(iconString, isDark)}
items={category.count}
selected={selectedCategory === category.name}
onClick={() => {
setSelectedCategory(category.name);
}}
/>
);
})}
</div>
<Statistics />
<Categories selected={selectedCategory} onSelectedChanged={setSelectedCategory}/>
</Explorer>
</>
);

View File

@@ -4,6 +4,7 @@ import { z } from 'zod';
import { useLibraryQuery } from '@sd/client';
import {
getExplorerStore,
SortOrder,
useExplorerStore,
useExplorerTopBarOptions,
useZodSearchParams
@@ -13,10 +14,10 @@ import { getExplorerItemData } from './Explorer/util';
import { TopBarPortal } from './TopBar/Portal';
import TopBarOptions from './TopBar/TopBarOptions';
const SEARCH_PARAMS = z.object({
export const SEARCH_PARAMS = z.object({
search: z.string().optional(),
take: z.coerce.number().optional(),
order: z.union([z.object({ name: z.boolean() }), z.object({ name: z.boolean() })]).optional()
order: z.union([z.object({ name: SortOrder }), z.object({ name: SortOrder })]).optional(),
});
export type SearchArgs = z.infer<typeof SEARCH_PARAMS>;
@@ -26,10 +27,20 @@ const ExplorerStuff = memo((props: { args: SearchArgs }) => {
const { explorerViewOptions, explorerControlOptions, explorerToolOptions } =
useExplorerTopBarOptions();
const query = useLibraryQuery(['search.paths', props.args], {
suspense: true,
enabled: !!props.args.search
});
const { search, ...args } = props.args;
const query = useLibraryQuery(
['search.paths', {
...args,
filter: {
search
},
}],
{
suspense: true,
enabled: !!search
}
);
const items = useMemo(() => {
const items = query.data?.items;
@@ -44,7 +55,7 @@ const ExplorerStuff = memo((props: { args: SearchArgs }) => {
useEffect(() => {
getExplorerStore().selectedRowIndex = null;
}, [props.args.search]);
}, [search]);
return (
<>
@@ -65,12 +76,12 @@ const ExplorerStuff = memo((props: { args: SearchArgs }) => {
</>
) : (
<div className="flex flex-1 flex-col items-center justify-center">
{!props.args.search && (
{!search && (
<MagnifyingGlass size={110} className="mb-5 text-ink-faint" opacity={0.3} />
)}
<p className="text-xs text-ink-faint">
{props.args.search
? `No results found for "${props.args.search}"`
{search
? `No results found for "${search}"`
: 'Search for files...'}
</p>
</div>

View File

@@ -13,7 +13,9 @@ export const Component = () => {
const explorerData = useLibraryQuery([
'search.objects',
{
tagId: id
filter: {
tags: [id]
}
}
]);

View File

@@ -1,8 +1,17 @@
import { proxy, useSnapshot } from 'valtio';
import { ExplorerItem, FilePathSearchOrdering } from '@sd/client';
import { resetStore } from '@sd/client/src/stores/util';
import { ExplorerItem, FilePathSearchOrdering, ObjectSearchOrdering } from '@sd/client';
import { resetStore } from '@sd/client';
import { z } from "zod"
type UnionKeys<T> = T extends any ? keyof T : never;
type Join<K, P> = K extends string | number
? P extends string | number
? `${K}${'' extends P ? '' : '.'}${P}`
: never
: never;
type Leaves<T> = T extends object ? { [K in keyof T]-?: Join<K, Leaves<T[K]>> }[keyof T] : '';
type UnionKeys<T> = T extends any ? Leaves<T> : never;
export type ExplorerLayoutMode = 'rows' | 'grid' | 'columns' | 'media';
@@ -14,9 +23,10 @@ export enum ExplorerKind {
export type CutCopyType = 'Cut' | 'Copy';
export type ExplorerOrderByKeys = UnionKeys<FilePathSearchOrdering> | 'none';
export type FilePathSearchOrderingKeys = UnionKeys<FilePathSearchOrdering> | 'none';
export type ObjectSearchOrderingKyes = UnionKeys<ObjectSearchOrdering> | 'none';
export type ExplorerDirection = 'asc' | 'desc';
export const SortOrder = z.union([z.literal("Asc"), z.literal("Desc")])
const state = {
locationId: null as number | null,
@@ -40,9 +50,9 @@ const state = {
isRenaming: false,
mediaColumns: 8,
mediaAspectSquare: true,
orderBy: 'dateCreated' as ExplorerOrderByKeys,
orderByDirection: 'desc' as ExplorerDirection,
groupBy: 'none',
orderBy: 'dateCreated' as FilePathSearchOrderingKeys,
orderByDirection: 'Desc' as z.infer<typeof SortOrder>,
groupBy: 'none'
};
// Keep the private and use `useExplorerState` or `getExplorerStore` or you will get production build issues.

View File

@@ -57,6 +57,7 @@
"remix-params-helper": "^0.4.10",
"rooks": "^5.14.0",
"tailwindcss": "^3.3.2",
"ts-deepmerge": "^6.0.3",
"use-count-up": "^3.0.1",
"use-debounce": "^8.0.4",
"valtio": "^1.7.4",

View File

@@ -4,7 +4,7 @@
export type Procedures = {
queries:
{ key: "buildInfo", input: never, result: BuildInfo } |
{ key: "categories.list", input: LibraryArgs<null>, result: CategoryItem[] } |
{ 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; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null; file_paths: FilePath[]; media_data: MediaData | null } | null } |
{ key: "jobs.getHistory", input: LibraryArgs<null>, result: JobReport[] } |
{ key: "jobs.getRunning", input: LibraryArgs<null>, result: JobReport[] } |
@@ -23,7 +23,7 @@ export type Procedures = {
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: IndexerRule } |
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: IndexerRule[] } |
{ key: "locations.indexer_rules.listForLocation", input: LibraryArgs<number>, result: IndexerRule[] } |
{ key: "locations.list", input: LibraryArgs<null>, result: ({ id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string; node: Node })[] } |
{ key: "locations.list", input: LibraryArgs<null>, result: { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string; node: Node }[] } |
{ key: "nodeState", input: never, result: NodeState } |
{ key: "search.objects", input: LibraryArgs<ObjectSearchArgs>, result: SearchData<ExplorerItem> } |
{ key: "search.paths", input: LibraryArgs<FilePathSearchArgs>, result: SearchData<ExplorerItem> } |
@@ -92,6 +92,8 @@ export type Procedures = {
{ key: "sync.newMessage", input: LibraryArgs<null>, result: CRDTOperation }
};
export type FilePathSearchArgs = { take?: number | null; order?: FilePathSearchOrdering | null; cursor?: number[] | null; filter?: FilePathFilterArgs }
export type PeerMetadata = { name: string; operating_system: OperatingSystem | null; version: string | null; email: string | null; img_url: string | null }
export type MasterPasswordChangeArgs = { password: Protected<string>; algorithm: Algorithm; hashing_algorithm: HashingAlgorithm }
@@ -101,10 +103,6 @@ export type MasterPasswordChangeArgs = { password: Protected<string>; algorithm:
*/
export type NodeConfig = { id: string; name: string; p2p_port: number | null; p2p_email: string | null; p2p_img_url: string | null }
export type CategoryItem = { name: string; count: number }
export type Location = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string }
/**
* This denotes the `StoredKey` version.
*/
@@ -119,12 +117,12 @@ export type EncryptedKey = number[]
export type PeerId = string
export type MediaData = { id: number; pixel_width: number | null; pixel_height: number | null; longitude: number | null; latitude: number | null; fps: number | null; capture_device_make: string | null; capture_device_model: string | null; capture_device_software: string | null; duration_seconds: number | null; codecs: string | null; streams: number | null }
export type GenerateThumbsForLocationArgs = { id: number; path: string }
export type LibraryConfigWrapped = { uuid: string; config: LibraryConfig }
export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string }
/**
* These parameters define the password-hashing level.
*
@@ -142,7 +140,7 @@ export type Params = "Standard" | "Hardened" | "Paranoid"
*/
export type LocationUpdateArgs = { id: number; name: string | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; indexer_rules_ids: number[] }
export type FilePathSearchArgs = { locationId?: number | null; afterFileId?: string | null; take?: number | null; order?: FilePathSearchOrdering | null; search?: string; extension?: string | null; kind?: number[]; tags?: number[]; createdAt?: OptionalRange<string>; path?: string | null; cursor?: number[] | null; favorite?: boolean | null; hidden?: boolean | null }
export type SortOrder = "Asc" | "Desc"
/**
* Represents the operating system which the remote peer is running.
@@ -150,6 +148,8 @@ export type FilePathSearchArgs = { locationId?: number | null; afterFileId?: str
*/
export type OperatingSystem = "Windows" | "Linux" | "MacOS" | "Ios" | "Android" | { Other: string }
export type GetArgs = { id: number }
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
/**
@@ -184,10 +184,12 @@ export type UnlockKeyManagerArgs = { password: Protected<string>; secret_key: Pr
export type NodeState = ({ id: string; name: string; p2p_port: number | null; p2p_email: string | null; p2p_img_url: string | null }) & { data_path: string }
export type SetFavoriteArgs = { id: number; favorite: boolean }
export type SetNoteArgs = { id: number; note: string | null }
export type InvalidateOperationEvent = { key: string; arg: any; result: any | null }
export type ObjectSearchArgs = { take?: number | null; order?: ObjectSearchOrdering | null; cursor?: number[] | null; filter?: ObjectFilterArgs }
export type CRDTOperation = { node: string; timestamp: number; id: string; typ: CRDTOperationType }
/**
@@ -197,17 +199,26 @@ export type CRDTOperation = { node: string; timestamp: number; id: string; typ:
*/
export type Salt = number[]
export type ObjectSearchArgs = { take?: number | null; tagId?: number | null; cursor?: number[] | null }
export type SetNoteArgs = { id: number; note: string | null }
export type FilePathSearchOrdering = { name: boolean } | { sizeInBytes: boolean } | { dateCreated: boolean } | { dateModified: boolean } | { dateIndexed: boolean } | { object: ObjectSearchOrdering }
export type FileCopierJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string; target_file_name_suffix: string | null }
/**
* Meow
*/
export type Category = "Recents" | "Favorites" | "Photos" | "Videos" | "Movies" | "Music" | "Documents" | "Downloads" | "Encrypted" | "Projects" | "Applications" | "Archives" | "Databases" | "Games" | "Books" | "Contacts" | "Trash"
export type Statistics = { id: number; date_captured: string; total_object_count: number; library_db_size: string; total_bytes_used: string; total_bytes_capacity: string; total_unique_bytes: string; total_bytes_free: string; preview_media_bytes: string }
export type FilePath = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string }
export type Node = { id: number; pub_id: number[]; name: string; platform: number; version: string | null; last_seen: string; timezone: string | null; date_created: string }
export type FileCopierJobInit = { source_location_id: number; source_path_id: number; target_location_id: number; target_path: string; target_file_name_suffix: string | null }
export type SetFavoriteArgs = { id: number; favorite: boolean }
export type Location = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string }
export type FilePathFilterArgs = { locationId?: number | null; search?: string; extension?: string | null; createdAt?: OptionalRange<string>; path?: string | null; object?: ObjectFilterArgs | null }
export type Object = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null }
export type FilePathSearchOrdering = { name: SortOrder } | { sizeInBytes: SortOrder } | { dateCreated: SortOrder } | { dateModified: SortOrder } | { dateIndexed: SortOrder } | { object: ObjectSearchOrdering }
export type BuildInfo = { version: string; commit: string }
@@ -218,27 +229,29 @@ export type IdentifyUniqueFilesArgs = { id: number; path: string }
*/
export type Algorithm = "XChaCha20Poly1305" | "Aes256Gcm"
export type ObjectSearchOrdering = { dateAccessed: boolean }
export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; total_objects: number | null; redundancy_goal: number | null; date_created: string; date_modified: string }
export type OwnedOperationItem = { id: any; data: OwnedOperationData }
export type MediaData = { id: number; pixel_width: number | null; pixel_height: number | null; longitude: number | null; latitude: number | null; fps: number | null; capture_device_make: string | null; capture_device_model: string | null; capture_device_software: string | null; duration_seconds: number | null; codecs: string | null; streams: number | null }
export type ObjectSearchOrdering = { dateAccessed: SortOrder }
export type CRDTOperationType = SharedOperation | RelationOperation | OwnedOperation
export type IndexerRule = { id: number; kind: number; name: string; default: boolean; parameters: number[]; date_created: string; date_modified: string }
/**
* TODO: P2P event for the frontend
*/
export type P2PEvent = { type: "DiscoveredPeer"; peer_id: PeerId; metadata: PeerMetadata } | { type: "SpacedropRequest"; id: string; peer_id: PeerId; name: string }
export type SpacedropArgs = { peer_id: PeerId; file_path: string[] }
export type RenameFileArgs = { location_id: number; file_name: string; new_file_name: string }
export type MaybeNot<T> = T | { not: T }
export type SpacedropArgs = { peer_id: PeerId; file_path: string[] }
export type JobReport = { id: string; name: string; action: string | null; data: number[] | null; metadata: any | null; is_background: boolean; errors_text: string[]; created_at: string | null; started_at: string | null; completed_at: string | null; parent_id: string | null; status: JobStatus; task_count: number; completed_task_count: number; message: string }
export type ObjectFilterArgs = { favorite?: boolean | null; hidden?: boolean | null; dateAccessed?: MaybeNot<string | null> | null; kind?: number[]; tags?: number[] }
export type OwnedOperation = { model: string; items: OwnedOperationItem[] }
export type ObjectWithFilePaths = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null; file_paths: FilePath[] }
@@ -265,7 +278,7 @@ export type SharedOperationCreateData = { u: { [key: string]: any } } | "a"
export type KeyAddArgs = { algorithm: Algorithm; hashing_algorithm: HashingAlgorithm; key: Protected<string>; library_sync: boolean; automount: boolean }
export type GetArgs = { id: number }
export type OptionalRange<T> = { from: T | null; to: T | null }
export type FileEncryptorJobInit = { location_id: number; path_id: number; key_uuid: string; algorithm: Algorithm; metadata: boolean; preview_media: boolean; output_path: string | null }
@@ -289,12 +302,6 @@ export type OwnedOperationData = { Create: { [key: string]: any } } | { CreateMa
export type SharedOperationData = SharedOperationCreateData | { field: string; value: any } | null
export type Tag = { id: number; pub_id: number[]; name: string | null; color: string | null; total_objects: number | null; redundancy_goal: number | null; date_created: string; date_modified: string }
export type SearchData<T> = { cursor: number[] | null; items: T[] }
export type OptionalRange<T> = { from: T | null; to: T | null }
export type TagUpdateArgs = { id: number; name: string | null; color: string | null }
export type ObjectValidatorArgs = { id: number; path: string }
@@ -303,8 +310,6 @@ export type TagAssignArgs = { object_id: number; tag_id: number; unassign: boole
export type ChangeNodeNameArgs = { name: string }
export type Object = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null }
/**
* This defines all available password hashing algorithms.
*/
@@ -312,23 +317,29 @@ export type HashingAlgorithm = { name: "Argon2id"; params: Params } | { name: "B
export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string; object: Object | null }
export type LocationWithIndexerRules = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string; indexer_rules: ({ indexer_rule: IndexerRule })[] }
export type LocationWithIndexerRules = { id: number; pub_id: number[]; node_id: number; name: string; path: string; total_capacity: number | null; available_capacity: number | null; is_archived: boolean; generate_preview_media: boolean; sync_preview_media: boolean; hidden: boolean; date_created: string; indexer_rules: { indexer_rule: IndexerRule }[] }
/**
* LibraryConfig holds the configuration for a specific library. This is stored as a '{uuid}.sdlibrary' file.
*/
export type LibraryConfig = { name: string; description: string }
export type SearchData<T> = { cursor: number[] | null; items: T[] }
export type CreateLibraryArgs = { name: string }
export type AutomountUpdateArgs = { uuid: string; status: boolean }
export type Protected<T> = T
export type FilePath = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string }
export type JobStatus = "Queued" | "Running" | "Completed" | "Canceled" | "Failed" | "Paused" | "CompletedWithErrors"
export type RestoreBackupArgs = { password: Protected<string>; secret_key: Protected<string>; path: string }
export type IndexerRule = { id: number; kind: number; name: string; default: boolean; parameters: number[]; date_created: string; date_modified: string }
export type RelationOperation = { relation_item: string; relation_group: string; relation: string; data: RelationOperationData }
/**

BIN
pnpm-lock.yaml generated
View File

Binary file not shown.