mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-26 01:19:10 -04:00
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:
BIN
Cargo.lock
generated
BIN
Cargo.lock
generated
Binary file not shown.
@@ -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" }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 ?? ''
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ export default function TagScreen({ navigation, route }: SharedScreenProps<'Tag'
|
||||
const search = useLibraryQuery([
|
||||
'search.objects',
|
||||
{
|
||||
tagId: id
|
||||
filter: {
|
||||
tags: [id]
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
50
interface/app/$libraryId/overview/Categories.tsx
Normal file
50
interface/app/$libraryId/overview/Categories.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
163
interface/app/$libraryId/overview/data.ts
Normal file
163
interface/app/$libraryId/overview/data.ts
Normal 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 {};
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,7 +13,9 @@ export const Component = () => {
|
||||
const explorerData = useLibraryQuery([
|
||||
'search.objects',
|
||||
{
|
||||
tagId: id
|
||||
filter: {
|
||||
tags: [id]
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
BIN
pnpm-lock.yaml
generated
Binary file not shown.
Reference in New Issue
Block a user