diff --git a/Cargo.lock b/Cargo.lock index f9b7f82d1..c4a81df3f 100644 Binary files a/Cargo.lock and b/Cargo.lock differ diff --git a/Cargo.toml b/Cargo.toml index a6f931a26..716d26cc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 41700fea6..0a193cc19 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -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] diff --git a/apps/desktop/src/commands.ts b/apps/desktop/src/commands.ts index e24da41c4..37d4c4a2c 100644 --- a/apps/desktop/src/commands.ts +++ b/apps/desktop/src/commands.ts @@ -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("app_ready") + return invoke()("app_ready") } export function openFilePath(library: string, id: number) { - return invoke("open_file_path", { library,id }) + return invoke()("open_file_path", { library,id }) } export function getFilePathOpenWithApps(library: string, id: number) { - return invoke("get_file_path_open_with_apps", { library,id }) + return invoke()("get_file_path_open_with_apps", { library,id }) } export function openFilePathWith(library: string, id: number, withUrl: string) { - return invoke("open_file_path_with", { library,id,withUrl }) + return invoke()("open_file_path_with", { library,id,withUrl }) } export type OpenWithApplication = { name: string; url: string } diff --git a/apps/mobile/src/screens/Location.tsx b/apps/mobile/src/screens/Location.tsx index 7ca64a3d0..a189746d6 100644 --- a/apps/mobile/src/screens/Location.tsx +++ b/apps/mobile/src/screens/Location.tsx @@ -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 ?? '' + } } ]); diff --git a/apps/mobile/src/screens/Tag.tsx b/apps/mobile/src/screens/Tag.tsx index 87898db8d..c585288a5 100644 --- a/apps/mobile/src/screens/Tag.tsx +++ b/apps/mobile/src/screens/Tag.tsx @@ -9,7 +9,9 @@ export default function TagScreen({ navigation, route }: SharedScreenProps<'Tag' const search = useLibraryQuery([ 'search.objects', { - tagId: id + filter: { + tags: [id] + } } ]); diff --git a/core/src/api/categories.rs b/core/src/api/categories.rs index 2a237203a..3b8d719a7 100644 --- a/core/src/api/categories.rs +++ b/core/src/api/categories.rs @@ -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 { 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) }) }) } diff --git a/core/src/api/files.rs b/core/src/api/files.rs index cb7f81992..49a85dd62 100644 --- a/core/src/api/files.rs +++ b/core/src/api/files.rs @@ -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 { R.router() diff --git a/core/src/api/search.rs b/core/src/api/search.rs index 39bc911a7..43060b393 100644 --- a/core/src/api/search.rs +++ b/core/src/api/search.rs @@ -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 { items: Vec, } -#[derive(Deserialize, Default, Type)] +#[derive(Deserialize, Default, Type, Debug)] #[serde(rename_all = "camelCase")] struct OptionalRange { from: Option, to: Option, } +#[derive(Deserialize, Type, Debug, Clone, Copy)] +enum SortOrder { + Asc, + Desc, +} + +impl Into 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), } 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 { + None(T), + Not { not: T }, +} + +impl MaybeNot { + fn to_prisma>>(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, - #[specta(optional)] - after_file_id: Option, - #[specta(optional)] - take: Option, - #[specta(optional)] - order: Option, #[serde(default)] search: String, #[specta(optional)] extension: Option, #[serde(default)] - kind: BTreeSet, - #[serde(default)] - tags: Vec, - #[serde(default)] created_at: OptionalRange>, #[specta(optional)] path: Option, #[specta(optional)] + object: Option, +} + +#[derive(Deserialize, Type, Debug)] +#[serde(rename_all = "camelCase")] +struct FilePathSearchArgs { + #[specta(optional)] + take: Option, + #[specta(optional)] + order: Option, + #[specta(optional)] cursor: Option>, - #[specta(optional)] - favorite: Option, - #[specta(optional)] - hidden: Option, + #[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, + #[specta(optional)] + hidden: Option, + #[specta(optional)] + date_accessed: Option>>>, + #[serde(default)] + kind: BTreeSet, + #[serde(default)] + tags: Vec, +} + +impl ObjectFilterArgs { + fn to_params(self) -> Vec { + 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, - #[serde(default)] #[specta(optional)] - tag_id: Option, + order: Option, #[specta(optional)] cursor: Option>, + #[serde(default)] + filter: ObjectFilterArgs, } pub fn mount() -> AlphaRouter { 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 { 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 { _ => 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 { } 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 { } Ok(SearchData { items, cursor }) - }) + }, + ) }) } diff --git a/core/src/library/cat.rs b/core/src/library/cat.rs index 71e0355ff..dfadfa80a 100644 --- a/core/src/library/cat.rs +++ b/core/src/library/cat.rs @@ -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, 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 } diff --git a/interface/app/$libraryId/Explorer/MediaView.tsx b/interface/app/$libraryId/Explorer/MediaView.tsx index 41db058a8..0f9a1e51d 100644 --- a/interface/app/$libraryId/Explorer/MediaView.tsx +++ b/interface/app/$libraryId/Explorer/MediaView.tsx @@ -32,7 +32,7 @@ const MediaViewItem = memo(({ data, index }: MediaViewItemProps) => { >
diff --git a/interface/app/$libraryId/Explorer/OptionsPanel.tsx b/interface/app/$libraryId/Explorer/OptionsPanel.tsx index b5384ad92..9d66cc8ef 100644 --- a/interface/app/$libraryId/Explorer/OptionsPanel.tsx +++ b/interface/app/$libraryId/Explorer/OptionsPanel.tsx @@ -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 = { +const sortOptions: Record = { 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) } > - Asc - Desc + {SortOrder.options.map(o => ( + {o.value} + ))}
diff --git a/interface/app/$libraryId/Explorer/util.ts b/interface/app/$libraryId/Explorer/util.ts index 2e1573d6f..cab3f1aa2 100644 --- a/interface/app/$libraryId/Explorer/util.ts +++ b/interface/app/$libraryId/Explorer/util.ts @@ -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) { diff --git a/interface/app/$libraryId/KeyManager/NotSetup.tsx b/interface/app/$libraryId/KeyManager/NotSetup.tsx index a51b1edb2..6053bd4bd 100644 --- a/interface/app/$libraryId/KeyManager/NotSetup.tsx +++ b/interface/app/$libraryId/KeyManager/NotSetup.tsx @@ -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({ diff --git a/interface/app/$libraryId/Layout/Sidebar/JobManager/Job.tsx b/interface/app/$libraryId/Layout/Sidebar/JobManager/Job.tsx index 67810b194..5243f59ff 100644 --- a/interface/app/$libraryId/Layout/Sidebar/JobManager/Job.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/JobManager/Job.tsx @@ -148,10 +148,10 @@ function Job({ job, clearJob, className, isGroup }: JobProps) { )} /> -
+
- {niceData.name} + {niceData.name}

{job.status === 'Queued' &&

{job.status}:

} {niceData.filesDiscovered} @@ -161,7 +161,7 @@ function Job({ job, clearJob, className, isGroup }: JobProps) {
-
+
{/* {job.status === 'Running' && (