diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml index 89cda1ffc..baf889960 100644 --- a/.github/actions/setup-pnpm/action.yml +++ b/.github/actions/setup-pnpm/action.yml @@ -9,9 +9,7 @@ runs: using: 'composite' steps: - name: Install pnpm - uses: pnpm/action-setup@v3 - with: - version: 9.0.6 + uses: pnpm/action-setup@v4 - name: Install Node.js uses: actions/setup-node@v4 diff --git a/.github/actions/setup-system/action.yml b/.github/actions/setup-system/action.yml index d6287844f..6b934c9e0 100644 --- a/.github/actions/setup-system/action.yml +++ b/.github/actions/setup-system/action.yml @@ -29,7 +29,7 @@ runs: - name: Install LLVM and Clang if: ${{ runner.os == 'Windows' }} - uses: KyleMayes/install-llvm-action@v1 + uses: KyleMayes/install-llvm-action@v2 with: cached: ${{ steps.cache-llvm-restore.outputs.cache-hit }} version: '15' diff --git a/.prettierignore b/.prettierignore index 4d06ac882..96e019a26 100644 --- a/.prettierignore +++ b/.prettierignore @@ -35,3 +35,5 @@ package*.json # Dont format locales json interface/locales + +scripts/utils/.tmp/* diff --git a/Cargo.lock b/Cargo.lock index 234727c72..a453654f5 100644 Binary files a/Cargo.lock and b/Cargo.lock differ diff --git a/Cargo.toml b/Cargo.toml index da4b5530a..2a215dfbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,31 +22,31 @@ rust-version = "1.81" # Third party dependencies used by one or more of our crates async-channel = "2.3" async-stream = "0.3.6" -async-trait = "0.1.80" -axum = "0.6.20" # Update blocked by hyper +async-trait = "0.1.83" +axum = "0.7.7" +axum-extra = "0.9.4" base64 = "0.22.1" blake3 = "1.5" chrono = "0.4.38" ed25519-dalek = "2.1" -futures = "0.3.30" +futures = "0.3.31" futures-concurrency = "7.6" globset = "0.4.15" -http = "0.2" # Update blocked by axum -hyper = "0.14" # Update blocked due to API breaking changes -image = "0.24.9" # Update blocked due to https://github.com/image-rs/image/issues/2230 +http = "1.1" +hyper = "1.5" +image = "0.24.9" # Update blocked due to https://github.com/image-rs/image/issues/2230 itertools = "0.13.0" lending-stream = "1.0" libc = "0.2" mimalloc = "0.1.43" -normpath = "1.2" +normpath = "1.3" pin-project-lite = "0.2.14" rand = "0.9.0-alpha.2" -regex = "1.11" -reqwest = { version = "0.11", default-features = false } # Update blocked by hyper +regex = "1" +reqwest = { version = "0.12.8", default-features = false } rmp = "0.8.14" rmp-serde = "1.3" rmpv = { version = "1.3", features = ["with-serde"] } -rspc = "0.1.4" # Update blocked by custom patch below serde = "1.0" serde_json = "1.0" specta = "=2.0.0-rc.20" @@ -60,45 +60,47 @@ tokio-util = "0.7.12" tracing = "0.1.40" tracing-subscriber = "0.3.18" tracing-test = "0.2.5" -uhlc = "0.8.0" # Must follow version used by specta -uuid = "1.10" # Must follow version used by specta -webp = "0.2.6" # Update blocked by image +uhlc = "0.8.0" # Must follow version used by specta +uuid = "1.10" # Must follow version used by specta +webp = "0.2.6" # Update blocked by image + +[workspace.dependencies.rspc] +git = "https://github.com/spacedriveapp/rspc.git" +rev = "6a77167495" [workspace.dependencies.prisma-client-rust] default-features = false features = ["migrations", "specta", "sqlite", "sqlite-create-many"] -git = "https://github.com/brendonovich/prisma-client-rust" -rev = "4f9ef9d38c" +git = "https://github.com/spacedriveapp/prisma-client-rust" +rev = "b22ad7dc7d" [workspace.dependencies.prisma-client-rust-sdk] default-features = false features = ["sqlite"] -git = "https://github.com/brendonovich/prisma-client-rust" -rev = "4f9ef9d38c" +git = "https://github.com/spacedriveapp/prisma-client-rust" +rev = "b22ad7dc7d" # Proper IOS Support [patch.crates-io.if-watch] git = "https://github.com/spacedriveapp/if-watch.git" rev = "a92c17d3f8" -# We use our own version of rspc -[patch.crates-io.rspc] -git = "https://github.com/spacedriveapp/rspc.git" -rev = "bc882f4724" - # Add `Control::open_stream_with_addrs` [patch.crates-io.libp2p] -git = "https://github.com/spacedriveapp/rust-libp2p.git" -rev = "a005656df7" +git = "https://github.com/spacedriveapp/rust-libp2p" +rev = "1024411ffa" [patch.crates-io.libp2p-core] -git = "https://github.com/spacedriveapp/rust-libp2p.git" -rev = "a005656df7" +git = "https://github.com/spacedriveapp/rust-libp2p" +rev = "1024411ffa" +[patch.crates-io.libp2p-identity] +git = "https://github.com/spacedriveapp/rust-libp2p" +rev = "1024411ffa" [patch.crates-io.libp2p-swarm] -git = "https://github.com/spacedriveapp/rust-libp2p.git" -rev = "a005656df7" +git = "https://github.com/spacedriveapp/rust-libp2p" +rev = "1024411ffa" [patch.crates-io.libp2p-stream] -git = "https://github.com/spacedriveapp/rust-libp2p.git" -rev = "a005656df7" +git = "https://github.com/spacedriveapp/rust-libp2p" +rev = "1024411ffa" [profile.dev] # Make compilation faster on macOS diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f6e07500a..999a28bae 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -12,15 +12,15 @@ "lint": "eslint src --cache" }, "dependencies": { - "@oscartbeaumont-sd/rspc-client": "github:spacedriveapp/rspc#path:packages/client&bc882f4724", - "@oscartbeaumont-sd/rspc-tauri": "github:spacedriveapp/rspc#path:packages/tauri&bc882f4724", + "@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495", + "@spacedrive/rspc-tauri": "github:spacedriveapp/rspc#path:packages/tauri&6a77167495", "@remix-run/router": "=1.13.1", "@sd/client": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", "@t3-oss/env-core": "^0.7.1", - "@tanstack/react-query": "^4.36.1", - "@tauri-apps/api": "=2.0.1", + "@tanstack/react-query": "^5.59", + "@tauri-apps/api": "=2.0.2", "@tauri-apps/plugin-dialog": "2.0.0", "@tauri-apps/plugin-os": "2.0.0", "@tauri-apps/plugin-shell": "2.0.0", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index de19d391d..2d0572fb0 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -16,7 +16,8 @@ sd-fda = { path = "../../../crates/fda" } sd-prisma = { path = "../../../crates/prisma" } # Workspace dependencies -axum = { workspace = true, features = ["headers", "query"] } +axum = { workspace = true, features = ["query"] } +axum-extra = { workspace = true, features = ["typed-header"] } futures = { workspace = true } http = { workspace = true } hyper = { workspace = true } @@ -36,9 +37,9 @@ uuid = { workspace = true, features = ["serde"] } # WARNING: Do NOT enable default features, as that vendors dbus (see below) opener = { version = "0.7.1", features = ["reveal"], default-features = false } specta-typescript = "=0.0.7" -tauri-plugin-dialog = "=2.0.1" +tauri-plugin-dialog = "=2.0.2" tauri-plugin-os = "=2.0.1" -tauri-plugin-shell = "=2.0.1" +tauri-plugin-shell = "=2.0.2" tauri-plugin-updater = "=2.0.2" # memory allocator @@ -46,12 +47,12 @@ mimalloc = { workspace = true } [dependencies.tauri] features = ["linux-libxdo", "macos-private-api", "native-tls-vendored", "unstable"] -version = "=2.0.1" +version = "=2.0.6" [dependencies.tauri-specta] features = ["derive", "typescript"] git = "https://github.com/spacedriveapp/tauri-specta" -rev = "1baf68be47" +rev = "8c85d40eb9" [target.'cfg(target_os = "linux")'.dependencies] # Spacedrive Sub-crates @@ -73,7 +74,7 @@ sd-desktop-windows = { path = "../crates/windows" } [build-dependencies] # Specific Desktop dependencies -tauri-build = "=2.0.1" +tauri-build = "=2.0.2" [features] ai-models = ["sd-core/ai"] diff --git a/apps/desktop/src-tauri/src/tauri_plugins.rs b/apps/desktop/src-tauri/src/tauri_plugins.rs index 6e2fe7eb4..bd8c9db81 100644 --- a/apps/desktop/src-tauri/src/tauri_plugins.rs +++ b/apps/desktop/src-tauri/src/tauri_plugins.rs @@ -1,20 +1,18 @@ -use std::{ - net::Ipv4Addr, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; +use std::{net::Ipv4Addr, sync::Arc}; use axum::{ - extract::{Query, State, TypedHeader}, - headers::authorization::{Authorization, Bearer}, + body::Body, + extract::{Query, State}, http::{Request, StatusCode}, middleware::{self, Next}, response::Response, RequestPartsExt, }; +use axum_extra::{ + headers::authorization::{Authorization, Bearer}, + TypedHeader, +}; use http::Method; -use hyper::server::{accept::Accept, conn::AddrIncoming}; use rand::{distr::Alphanumeric, Rng}; use sd_core::{custom_uri, Node, NodeError}; use serde::Deserialize; @@ -66,29 +64,14 @@ pub async fn sd_server_plugin( .fallback(|| async { "404 Not Found: We're past the event horizon..." }); // Only allow current device to access it - let listenera = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?; - let listen_addra = listenera.local_addr()?; - let listenerb = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?; - let listen_addrb = listenerb.local_addr()?; - let listenerc = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?; - let listen_addrc = listenerc.local_addr()?; - let listenerd = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?; - let listen_addrd = listenerd.local_addr()?; - - // let listen_addr = listener.local_addr()?; // We get it from a listener so `0` is turned into a random port + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?; + let listen_addr = listener.local_addr()?; // We get it from a listener so `0` is turned into a random port let (tx, mut rx) = tokio::sync::mpsc::channel(1); - info!("Internal server listening on: http://{listen_addra:?} http://{listen_addrb:?} http://{listen_addrc:?} http://{listen_addrd:?}"); - let server = axum::Server::builder(CombinedIncoming { - a: AddrIncoming::from_listener(listenera)?, - b: AddrIncoming::from_listener(listenerb)?, - c: AddrIncoming::from_listener(listenerc)?, - d: AddrIncoming::from_listener(listenerd)?, - }); + info!("Internal server listening on: http://{listen_addr:?}"); tokio::spawn(async move { - server - .serve(app.into_make_service()) - .with_graceful_shutdown(async { + axum::serve(listener, app) + .with_graceful_shutdown(async move { rx.recv().await; }) .await @@ -96,12 +79,7 @@ pub async fn sd_server_plugin( }); let script = format!( - r#"window.__SD_CUSTOM_SERVER_AUTH_TOKEN__ = "{auth_token}"; window.__SD_CUSTOM_URI_SERVER__ = [{}];"#, - [listen_addra, listen_addrb, listen_addrc, listen_addrd] - .iter() - .map(|addr| format!("'http://{addr}'")) - .collect::>() - .join(","), + r#"window.__SD_CUSTOM_SERVER_AUTH_TOKEN__ = "{auth_token}"; window.__SD_CUSTOM_URI_SERVER__ = ['http://{listen_addr}'];"#, ); Ok(tauri::plugin::Builder::new("sd-server") @@ -127,15 +105,12 @@ struct QueryParams { token: Option, } -async fn auth_middleware( +async fn auth_middleware( Query(query): Query, State(auth_token): State, - request: Request, - next: Next, -) -> Result -where - B: Send, -{ + request: Request, + next: Next, +) -> Result { let req = if query.token.as_ref() != Some(&auth_token) { let (mut parts, body) = request.into_parts(); @@ -158,38 +133,3 @@ where Ok(next.run(req).await) } - -struct CombinedIncoming { - a: AddrIncoming, - b: AddrIncoming, - c: AddrIncoming, - d: AddrIncoming, -} - -impl Accept for CombinedIncoming { - type Conn = ::Conn; - type Error = ::Error; - - fn poll_accept( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - if let Poll::Ready(Some(value)) = Pin::new(&mut self.a).poll_accept(cx) { - return Poll::Ready(Some(value)); - } - - if let Poll::Ready(Some(value)) = Pin::new(&mut self.b).poll_accept(cx) { - return Poll::Ready(Some(value)); - } - - if let Poll::Ready(Some(value)) = Pin::new(&mut self.c).poll_accept(cx) { - return Poll::Ready(Some(value)); - } - - if let Poll::Ready(Some(value)) = Pin::new(&mut self.d).poll_accept(cx) { - return Poll::Ready(Some(value)); - } - - Poll::Pending - } -} diff --git a/apps/desktop/src/patches.ts b/apps/desktop/src/patches.ts index 8fd5d4500..f1c33a0a7 100644 --- a/apps/desktop/src/patches.ts +++ b/apps/desktop/src/patches.ts @@ -1,4 +1,4 @@ -import { tauriLink } from '@oscartbeaumont-sd/rspc-tauri/src/v2'; +import { tauriLink } from '@spacedrive/rspc-tauri/src/v2'; globalThis.isDev = import.meta.env.DEV; globalThis.rspcLinks = [ diff --git a/apps/landing/package.json b/apps/landing/package.json index 23fedacdf..6cc22e363 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -49,7 +49,7 @@ "three": "^0.161.0", "tsparticles": "^3.3.0", "unist-util-visit": "^5.0.0", - "zod": "~3.22.4" + "zod": "^3.23" }, "devDependencies": { "@next/bundle-analyzer": "^13.5.6", diff --git a/apps/mobile/modules/sd-core/src/index.ts b/apps/mobile/modules/sd-core/src/index.ts index fb034782a..dfb338f36 100644 --- a/apps/mobile/modules/sd-core/src/index.ts +++ b/apps/mobile/modules/sd-core/src/index.ts @@ -1,4 +1,4 @@ -import { AlphaRSPCError, Link, RspcRequest } from '@oscartbeaumont-sd/rspc-client/src/v2'; +import { Link, RSPCError, RspcRequest } from '@spacedrive/rspc-client'; import { EventEmitter, requireNativeModule } from 'expo-modules-core'; // It loads the native module object from the JSI or falls back to @@ -15,7 +15,7 @@ export function reactNativeLink(): Link { string, { resolve: (result: any) => void; - reject: (error: Error | AlphaRSPCError) => void; + reject: (error: Error | RSPCError) => void; } >(); @@ -29,7 +29,7 @@ export function reactNativeLink(): Link { activeMap.delete(id); } else if (result.type === 'error') { const { message, code } = result.data; - activeMap.get(id)?.reject(new AlphaRSPCError(code, message)); + activeMap.get(id)?.reject(new RSPCError(code, message)); activeMap.delete(id); } else { console.error(`rspc: received event of unknown type '${result.type}'`); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 5dc682437..525c790ba 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -21,8 +21,8 @@ "@dr.pogodin/react-native-fs": "^2.24.1", "@gorhom/bottom-sheet": "^4.6.1", "@hookform/resolvers": "^3.1.0", - "@oscartbeaumont-sd/rspc-client": "github:spacedriveapp/rspc#path:packages/client&bc882f4724", - "@oscartbeaumont-sd/rspc-react": "github:spacedriveapp/rspc#path:packages/react&bc882f4724", + "@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495", + "@spacedrive/rspc-react": "github:spacedriveapp/rspc#path:packages/react&6a77167495", "@react-native-async-storage/async-storage": "~1.23.1", "@react-native-masked-view/masked-view": "^0.3.1", "@react-navigation/bottom-tabs": "^6.5.19", @@ -32,7 +32,7 @@ "@sd/assets": "workspace:*", "@sd/client": "workspace:*", "@shopify/flash-list": "1.6.4", - "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query": "^5.59", "babel-preset-solid": "^1.9.0", "class-variance-authority": "^0.7.0", "dayjs": "^1.11.10", @@ -74,8 +74,8 @@ "twrnc": "^4.1.0", "use-count-up": "^3.0.1", "use-debounce": "^9.0.4", - "valtio": "^1.11.2", - "zod": "~3.22.4" + "valtio": "^2.0", + "zod": "^3.23" }, "devDependencies": { "@babel/core": "^7.24.0", diff --git a/apps/mobile/src/components/browse/BrowseLocations.tsx b/apps/mobile/src/components/browse/BrowseLocations.tsx index 75787d7a4..4159fbe7f 100644 --- a/apps/mobile/src/components/browse/BrowseLocations.tsx +++ b/apps/mobile/src/components/browse/BrowseLocations.tsx @@ -1,4 +1,5 @@ import { useNavigation } from '@react-navigation/native'; +import { keepPreviousData } from '@tanstack/react-query'; import { Plus } from 'phosphor-react-native'; import { useRef, useState } from 'react'; import { FlatList, Text, View } from 'react-native'; @@ -22,7 +23,7 @@ const BrowseLocations = () => { const modalRef = useRef(null); const [showAll, setShowAll] = useState(false); - const result = useLibraryQuery(['locations.list'], { keepPreviousData: true }); + const result = useLibraryQuery(['locations.list'], { placeholderData: keepPreviousData }); const locations = result.data; return ( diff --git a/apps/mobile/src/components/drawer/DrawerLocations.tsx b/apps/mobile/src/components/drawer/DrawerLocations.tsx index 2e1d8995c..0a5f87559 100644 --- a/apps/mobile/src/components/drawer/DrawerLocations.tsx +++ b/apps/mobile/src/components/drawer/DrawerLocations.tsx @@ -1,5 +1,6 @@ import { DrawerNavigationHelpers } from '@react-navigation/drawer/lib/typescript/src/types'; import { useNavigation } from '@react-navigation/native'; +import { keepPreviousData } from '@tanstack/react-query'; import { useRef } from 'react'; import { Pressable, Text, View } from 'react-native'; import { @@ -73,7 +74,7 @@ const DrawerLocations = () => { const modalRef = useRef(null); - const result = useLibraryQuery(['locations.list'], { keepPreviousData: true }); + const result = useLibraryQuery(['locations.list'], { placeholderData: keepPreviousData }); const locations = result.data || []; return ( diff --git a/apps/mobile/src/components/explorer/Explorer.tsx b/apps/mobile/src/components/explorer/Explorer.tsx index 10b448d7c..d3281aa7d 100644 --- a/apps/mobile/src/components/explorer/Explorer.tsx +++ b/apps/mobile/src/components/explorer/Explorer.tsx @@ -1,6 +1,6 @@ import { useNavigation } from '@react-navigation/native'; import { FlashList } from '@shopify/flash-list'; -import { UseInfiniteQueryResult } from '@tanstack/react-query'; +import { InfiniteData, UseInfiniteQueryResult } from '@tanstack/react-query'; import * as Haptics from 'expo-haptics'; import { useRef } from 'react'; import { ActivityIndicator } from 'react-native'; @@ -32,7 +32,7 @@ type ExplorerProps = { items: ExplorerItem[] | null; /** Function to fetch next page of items. */ loadMore: () => void; - query: UseInfiniteQueryResult>; + query: UseInfiniteQueryResult>>; count?: number; empty?: never; isEmpty?: never; diff --git a/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx b/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx index 5a09e515c..2dd851b7a 100644 --- a/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx +++ b/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx @@ -12,7 +12,7 @@ type Props = { const FavoriteButton = (props: Props) => { const [favorite, setFavorite] = useState(props.data.favorite); - const { mutate: toggleFavorite, isLoading } = useLibraryMutation('files.setFavorite', { + const { mutate: toggleFavorite, isPending } = useLibraryMutation('files.setFavorite', { onSuccess: () => { // TODO: Invalidate search queries setFavorite(!favorite); @@ -22,7 +22,7 @@ const FavoriteButton = (props: Props) => { return ( toggleFavorite({ id: props.data.id, favorite: !favorite })} style={props.style} > diff --git a/apps/mobile/src/components/job/JobGroup.tsx b/apps/mobile/src/components/job/JobGroup.tsx index 26dc832ac..9000a7b0a 100644 --- a/apps/mobile/src/components/job/JobGroup.tsx +++ b/apps/mobile/src/components/job/JobGroup.tsx @@ -191,7 +191,7 @@ function Options({ activeJob, group, setShowChildJobs, showChildJobs }: OptionsP const clearJob = useLibraryMutation(['jobs.clear'], { onSuccess: () => { - rspc.queryClient.invalidateQueries(['jobs.reports']); + rspc.queryClient.invalidateQueries({ queryKey: ['jobs.reports'] }); } }); diff --git a/apps/mobile/src/components/modal/AddTagModal.tsx b/apps/mobile/src/components/modal/AddTagModal.tsx index 929820c4c..ae69a74ff 100644 --- a/apps/mobile/src/components/modal/AddTagModal.tsx +++ b/apps/mobile/src/components/modal/AddTagModal.tsx @@ -35,8 +35,8 @@ const AddTagModal = forwardRef((_, ref) => { const mutation = useLibraryMutation(['tags.assign'], { onSuccess: () => { // this makes sure that the tags are updated in the UI - rspc.queryClient.invalidateQueries(['tags.getForObject']); - rspc.queryClient.invalidateQueries(['search.paths']); + rspc.queryClient.invalidateQueries({ queryKey: ['tags.getForObject'] }); + rspc.queryClient.invalidateQueries({ queryKey: ['search.paths'] }); modalRef.current?.dismiss(); } }); diff --git a/apps/mobile/src/components/modal/CreateLibraryModal.tsx b/apps/mobile/src/components/modal/CreateLibraryModal.tsx index 346988473..83d005bfd 100644 --- a/apps/mobile/src/components/modal/CreateLibraryModal.tsx +++ b/apps/mobile/src/components/modal/CreateLibraryModal.tsx @@ -17,7 +17,7 @@ const CreateLibraryModal = forwardRef((_, ref) => { const submitPlausibleEvent = usePlausibleEvent(); - const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation( + const { mutate: createLibrary, isPending: createLibLoading } = useBridgeMutation( 'library.create', { onSuccess: (lib) => { diff --git a/apps/mobile/src/components/modal/ImportLibraryModal.tsx b/apps/mobile/src/components/modal/ImportLibraryModal.tsx index 82de9fdd4..f15edb421 100644 --- a/apps/mobile/src/components/modal/ImportLibraryModal.tsx +++ b/apps/mobile/src/components/modal/ImportLibraryModal.tsx @@ -100,7 +100,7 @@ const CloudLibraryCard = ({ data, modalRef, navigation }: Props) => { diff --git a/apps/mobile/src/components/overview/Devices.tsx b/apps/mobile/src/components/overview/Devices.tsx index 8643d08af..1f65407b9 100644 --- a/apps/mobile/src/components/overview/Devices.tsx +++ b/apps/mobile/src/components/overview/Devices.tsx @@ -1,5 +1,5 @@ import * as RNFS from '@dr.pogodin/react-native-fs'; -import { AlphaRSPCError } from '@oscartbeaumont-sd/rspc-client/src/v2'; +import { RSPCError } from '@spacedrive/rspc-client'; import { UseQueryResult } from '@tanstack/react-query'; import React, { useEffect, useState } from 'react'; import { Platform, Text, View } from 'react-native'; @@ -16,7 +16,7 @@ import StatCard from './StatCard'; interface Props { node: NodeState | undefined; - stats: UseQueryResult; + stats: UseQueryResult; } export function hardwareModelToIcon(hardwareModel: HardwareModel) { diff --git a/apps/mobile/src/components/overview/OverviewStats.tsx b/apps/mobile/src/components/overview/OverviewStats.tsx index 2a7ecd0de..3ba73a8ce 100644 --- a/apps/mobile/src/components/overview/OverviewStats.tsx +++ b/apps/mobile/src/components/overview/OverviewStats.tsx @@ -1,5 +1,5 @@ import * as RNFS from '@dr.pogodin/react-native-fs'; -import { AlphaRSPCError } from '@oscartbeaumont-sd/rspc-client/src/v2'; +import { RSPCError } from '@spacedrive/rspc-client'; import { UseQueryResult } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import { Platform, Text, View } from 'react-native'; @@ -47,7 +47,7 @@ const StatItem = ({ title, bytes, isLoading, style }: StatItemProps) => { }; interface Props { - stats: UseQueryResult; + stats: UseQueryResult; } const OverviewStats = ({ stats }: Props) => { diff --git a/apps/mobile/src/components/search/filters/SavedSearches.tsx b/apps/mobile/src/components/search/filters/SavedSearches.tsx index e4dacb47e..5a63ac22f 100644 --- a/apps/mobile/src/components/search/filters/SavedSearches.tsx +++ b/apps/mobile/src/components/search/filters/SavedSearches.tsx @@ -71,7 +71,7 @@ const SavedSearch = ({ search }: Props) => { const dataForSearch = useSavedSearch(search); const rspc = useRspcLibraryContext(); const deleteSearch = useLibraryMutation('search.saved.delete', { - onSuccess: () => rspc.queryClient.invalidateQueries(['search.saved.list']) + onSuccess: () => rspc.queryClient.invalidateQueries({ queryKey: ['search.saved.list'] }) }); return ( { diff --git a/apps/mobile/src/hooks/useSavedSearch.ts b/apps/mobile/src/hooks/useSavedSearch.ts index 0bdaa7e59..cf61b0ecc 100644 --- a/apps/mobile/src/hooks/useSavedSearch.ts +++ b/apps/mobile/src/hooks/useSavedSearch.ts @@ -1,4 +1,5 @@ import { IconTypes } from '@sd/assets/util'; +import { keepPreviousData } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; import { SavedSearch, SearchFilterArgs, Tag, useLibraryQuery } from '@sd/client'; import { kinds } from '~/components/search/filters/Kind'; @@ -44,11 +45,11 @@ export function useSavedSearch(search: SavedSearch) { }; const locations = useLibraryQuery(['locations.list'], { - keepPreviousData: true, + placeholderData: keepPreviousData, enabled: filterKeys.includes('locations') }); const tags = useLibraryQuery(['tags.list'], { - keepPreviousData: true, + placeholderData: keepPreviousData, enabled: filterKeys.includes('tags') }); diff --git a/apps/mobile/src/screens/BackfillWaiting.tsx b/apps/mobile/src/screens/BackfillWaiting.tsx index ab13c54e7..10f7e76ba 100644 --- a/apps/mobile/src/screens/BackfillWaiting.tsx +++ b/apps/mobile/src/screens/BackfillWaiting.tsx @@ -52,10 +52,8 @@ const BackfillWaiting = () => { const syncEnabled = useLibraryQuery(['sync.enabled']); useEffect(() => { - (async () => { - await enableSync.mutateAsync(null); - })(); - }, []); + enableSync.mutate(null); + }, [enableSync]); return ( diff --git a/apps/mobile/src/screens/browse/Location.tsx b/apps/mobile/src/screens/browse/Location.tsx index 81f9a152c..ca1bff307 100644 --- a/apps/mobile/src/screens/browse/Location.tsx +++ b/apps/mobile/src/screens/browse/Location.tsx @@ -62,10 +62,13 @@ export default function LocationScreen({ navigation, route }: BrowseStackScreenP filters: [...defaultFilters, ...layoutFilter].filter(Boolean), take: 30 }, - order, - onSuccess: () => getExplorerStore().resetNewThumbnails() + order }); + useEffect(() => { + getExplorerStore().resetNewThumbnails(); + }, [path]); + useEffect(() => { // Set screen title to location. if (path && path !== '') { diff --git a/apps/mobile/src/screens/search/Search.tsx b/apps/mobile/src/screens/search/Search.tsx index f0f0a3ad0..bf6dd3a0f 100644 --- a/apps/mobile/src/screens/search/Search.tsx +++ b/apps/mobile/src/screens/search/Search.tsx @@ -1,6 +1,6 @@ import { useIsFocused } from '@react-navigation/native'; import { ArrowLeft, DotsThree, FunnelSimple } from 'phosphor-react-native'; -import { Suspense, useDeferredValue, useMemo, useState } from 'react'; +import { Suspense, useDeferredValue, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Platform, Pressable, TextInput, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { ObjectKindEnum, useLibraryQuery, usePathsExplorerQuery } from '@sd/client'; @@ -41,10 +41,11 @@ const SearchScreen = ({ navigation }: SearchStackScreenProps<'Search'>) => { filters: [...layoutSearchFilter, ...searchStore.mergedFilters] }, enabled: isFocused && searchStore.mergedFilters.length >= 1, // only fetch when screen is focused & filters are applied - suspense: true, - onSuccess: () => getExplorerStore().resetNewThumbnails() + suspense: true }); + useEffect(() => getExplorerStore().resetNewThumbnails(), [objects]); + useFiltersSearch(deferredSearch); const appliedFiltersLength = Object.keys(searchStore.appliedFilters).length; diff --git a/apps/mobile/src/screens/settings/library/CloudSettings/CloudSettings.tsx b/apps/mobile/src/screens/settings/library/CloudSettings/CloudSettings.tsx index f121eb8c4..892556d0e 100644 --- a/apps/mobile/src/screens/settings/library/CloudSettings/CloudSettings.tsx +++ b/apps/mobile/src/screens/settings/library/CloudSettings/CloudSettings.tsx @@ -111,10 +111,10 @@ const Authenticated = () => { ); @@ -147,11 +147,11 @@ function StopButton({ name }: { name: string }) { ); diff --git a/apps/mobile/src/stores/auth.ts b/apps/mobile/src/stores/auth.ts index 3ef1079d8..336b3ff22 100644 --- a/apps/mobile/src/stores/auth.ts +++ b/apps/mobile/src/stores/auth.ts @@ -1,4 +1,4 @@ -import { RSPCError } from '@oscartbeaumont-sd/rspc-client'; +import { RSPCError } from '@spacedrive/rspc-client'; import { Linking } from 'react-native'; import { createMutable } from 'solid-js/store'; import { nonLibraryClient, useSolidStore } from '@sd/client'; diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index f649ab23b..f4050942f 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -16,12 +16,13 @@ default = [] sd-core = { path = "../../core", features = ["ffmpeg", "heif"] } # Workspace dependencies -axum = { workspace = true, features = ["headers"] } -http = { workspace = true } -rspc = { workspace = true, features = ["axum"] } -tempfile = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "signal", "sync"] } -tracing = { workspace = true } +axum = { workspace = true } +axum-extra = { workspace = true, features = ["typed-header"] } +http = { workspace = true } +rspc = { workspace = true, features = ["axum"] } +tempfile = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "signal", "sync"] } +tracing = { workspace = true } # Specific Desktop dependencies include_dir = "0.7.3" diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index 252d45676..5a6304bf3 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -1,12 +1,15 @@ use std::{collections::HashMap, env, net::SocketAddr, path::Path}; use axum::{ + body::Body, extract::{FromRequestParts, State}, - headers::{authorization::Basic, Authorization}, http::Request, middleware::Next, response::{IntoResponse, Response}, routing::get, +}; +use axum_extra::{ + headers::{authorization::Basic, Authorization}, TypedHeader, }; use sd_core::{custom_uri, Node}; @@ -24,11 +27,7 @@ pub struct AppState { auth: HashMap, } -async fn basic_auth( - State(state): State, - request: Request, - next: Next, -) -> Response { +async fn basic_auth(State(state): State, request: Request, next: Next) -> Response { let request = if !state.auth.is_empty() { let (mut parts, body) = request.into_parts(); @@ -175,10 +174,7 @@ async fn main() { .route( "/", get(|| async move { - use axum::{ - body::{self, Full}, - response::Response, - }; + use axum::{body::Body, response::Response}; use http::{header, HeaderValue, StatusCode}; match ASSETS_DIR.get_file("index.html") { @@ -188,11 +184,11 @@ async fn main() { header::CONTENT_TYPE, HeaderValue::from_str("text/html").unwrap(), ) - .body(body::boxed(Full::from(file.contents()))) + .body(Body::from(file.contents())) .unwrap(), None => Response::builder() .status(StatusCode::NOT_FOUND) - .body(body::boxed(axum::body::Empty::new())) + .body(Body::empty()) .unwrap(), } }), @@ -201,10 +197,7 @@ async fn main() { "/*id", get( |axum::extract::Path(path): axum::extract::Path| async move { - use axum::{ - body::{self, Empty, Full}, - response::Response, - }; + use axum::{body::Body, response::Response}; use http::{header, HeaderValue, StatusCode}; let path = path.trim_start_matches('/'); @@ -218,7 +211,7 @@ async fn main() { ) .unwrap(), ) - .body(body::boxed(Full::from(file.contents()))) + .body(Body::from(file.contents())) .unwrap(), None => match ASSETS_DIR.get_file("index.html") { Some(file) => Response::builder() @@ -227,11 +220,11 @@ async fn main() { header::CONTENT_TYPE, HeaderValue::from_str("text/html").unwrap(), ) - .body(body::boxed(Full::from(file.contents()))) + .body(Body::from(file.contents())) .unwrap(), None => Response::builder() .status(StatusCode::NOT_FOUND) - .body(body::boxed(Empty::new())) + .body(Body::empty()) .unwrap(), }, } @@ -254,8 +247,7 @@ async fn main() { let mut addr = "[::]:8080".parse::().unwrap(); // This listens on IPv6 and IPv4 addr.set_port(port); info!("Listening on http://localhost:{}", port); - axum::Server::bind(&addr) - .serve(app.into_make_service()) + axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app) .with_graceful_shutdown(signal) .await .expect("Error with HTTP server!"); diff --git a/apps/web/package.json b/apps/web/package.json index b487de1d8..ad2a51ebf 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,10 +17,10 @@ "lint": "eslint src --cache" }, "dependencies": { - "@oscartbeaumont-sd/rspc-client": "github:spacedriveapp/rspc#path:packages/client&bc882f4724", + "@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495", "@sd/client": "workspace:*", "@sd/interface": "workspace:*", - "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query": "^5.59", "html-to-image": "^1.11.11", "html2canvas": "^1.4.1", "react": "^18.2.0", diff --git a/apps/web/src/patches.ts b/apps/web/src/patches.ts index 2faac6997..a545a1217 100644 --- a/apps/web/src/patches.ts +++ b/apps/web/src/patches.ts @@ -1,4 +1,4 @@ -import { wsBatchLink } from '@oscartbeaumont-sd/rspc-client/src/v2'; +import { wsBatchLink } from '@spacedrive/rspc-client'; globalThis.isDev = import.meta.env.DEV; globalThis.rspcLinks = [ diff --git a/core/Cargo.toml b/core/Cargo.toml index 49aac5516..3d3762464 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -64,7 +64,7 @@ reqwest = { workspace = true, features = ["json", "native-tls-vendor rmp-serde = { workspace = true } rmpv = { workspace = true } rspc = { workspace = true, features = ["alpha", "axum", "chrono", "unstable", "uuid"] } -serde = { workspace = true, features = ["derive"] } +serde = { workspace = true, features = ["derive", "rc"] } serde_json = { workspace = true } specta = { workspace = true } strum = { workspace = true, features = ["derive"] } @@ -85,15 +85,16 @@ ctor = "0.2.8" directories = "5.0" flate2 = "1.0" hostname = "0.4.0" -http-body = "0.4.6" # Update blocked by http +http-body = "1.0" http-range = "0.1.5" -int-enum = "0.5" # Update blocked due to API breaking changes +hyper-util = { version = "0.1.9", features = ["tokio"] } +int-enum = "0.5" # Update blocked due to API breaking changes mini-moka = "0.10.3" serde-hashkey = "0.4.5" serde_repr = "0.1.19" serde_with = "3.8" slotmap = "1.0" -sysinfo = "0.29.11" # Update blocked due to API breaking changes +sysinfo = "0.29.11" # Update blocked due to API breaking changes tar = "0.4.41" tower-service = "0.3.2" tracing-appender = "0.2.3" diff --git a/core/crates/heavy-lifting/Cargo.toml b/core/crates/heavy-lifting/Cargo.toml index 75e99359c..20743e9fa 100644 --- a/core/crates/heavy-lifting/Cargo.toml +++ b/core/crates/heavy-lifting/Cargo.toml @@ -44,7 +44,7 @@ prisma-client-rust = { workspace = true } rmp-serde = { workspace = true } rmpv = { workspace = true } rspc = { workspace = true } -serde = { workspace = true, features = ["derive"] } +serde = { workspace = true, features = ["derive", "rc"] } serde_json = { workspace = true } specta = { workspace = true } strum = { workspace = true, features = ["derive", "phf"] } diff --git a/core/crates/heavy-lifting/src/media_processor/helpers/thumbnailer.rs b/core/crates/heavy-lifting/src/media_processor/helpers/thumbnailer.rs index d1637ebb3..6d919fbae 100644 --- a/core/crates/heavy-lifting/src/media_processor/helpers/thumbnailer.rs +++ b/core/crates/heavy-lifting/src/media_processor/helpers/thumbnailer.rs @@ -25,7 +25,8 @@ use image::{imageops, DynamicImage, GenericImageView}; use serde::{Deserialize, Serialize}; use specta::Type; use tokio::{ - fs, io, + fs::{self, File}, + io::{self, AsyncWriteExt}, sync::{oneshot, Mutex}, task::spawn_blocking, time::{sleep, Instant}, @@ -450,15 +451,29 @@ async fn generate_image_thumbnail( trace!("Created shard directory and writing it to disk"); - let res = fs::write(output_path, &webp).await.map_err(|e| { + let mut file = File::create(output_path).await.map_err(|e| { + thumbnailer::NonCriticalThumbnailerError::SaveThumbnail( + file_path.clone(), + FileIOError::from((output_path, e)).to_string(), + ) + })?; + + file.write_all(&webp).await.map_err(|e| { + thumbnailer::NonCriticalThumbnailerError::SaveThumbnail( + file_path.clone(), + FileIOError::from((output_path, e)).to_string(), + ) + })?; + + file.sync_all().await.map_err(|e| { thumbnailer::NonCriticalThumbnailerError::SaveThumbnail( file_path, FileIOError::from((output_path, e)).to_string(), ) - }); + })?; trace!("Wrote thumbnail to disk"); - res + return Ok(()); } #[instrument( diff --git a/core/crates/heavy-lifting/src/media_processor/tasks/thumbnailer.rs b/core/crates/heavy-lifting/src/media_processor/tasks/thumbnailer.rs index 0180014a9..1497e3cc4 100644 --- a/core/crates/heavy-lifting/src/media_processor/tasks/thumbnailer.rs +++ b/core/crates/heavy-lifting/src/media_processor/tasks/thumbnailer.rs @@ -379,21 +379,20 @@ fn process_thumbnail_generation_output( match status { GenerationStatus::Generated => { *generated += 1; + // This if is REALLY needed, due to the sheer performance of the thumbnailer, + // I restricted to only send events notifying for thumbnails in the current + // opened directory, sending events for the entire location turns into a + // humongous bottleneck in the frontend lol, since it doesn't even knows + // what to do with thumbnails for inner directories lol + // - fogodev + if with_priority { + reporter.new_thumbnail(thumb_key); + } } GenerationStatus::Skipped => { *skipped += 1; } } - - // This if is REALLY needed, due to the sheer performance of the thumbnailer, - // I restricted to only send events notifying for thumbnails in the current - // opened directory, sending events for the entire location turns into a - // humongous bottleneck in the frontend lol, since it doesn't even knows - // what to do with thumbnails for inner directories lol - // - fogodev - if with_priority { - reporter.new_thumbnail(thumb_key); - } } Err(e) => { errors.push(media_processor::NonCriticalMediaProcessorError::from(e).into()); diff --git a/core/crates/indexer-rules/Cargo.toml b/core/crates/indexer-rules/Cargo.toml index 218f04d75..472bd2442 100644 --- a/core/crates/indexer-rules/Cargo.toml +++ b/core/crates/indexer-rules/Cargo.toml @@ -19,7 +19,7 @@ globset = { workspace = true, features = ["serde1"] } prisma-client-rust = { workspace = true } rmp-serde = { workspace = true } rspc = { workspace = true } -serde = { workspace = true, features = ["derive"] } +serde = { workspace = true, features = ["derive", "rc"] } specta = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["fs"] } diff --git a/core/src/custom_uri/async_read_body.rs b/core/src/custom_uri/async_read_body.rs deleted file mode 100644 index 1a1cc523a..000000000 --- a/core/src/custom_uri/async_read_body.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::{ - io, - pin::Pin, - task::{Context, Poll}, -}; - -use axum::http::HeaderMap; -use bytes::Bytes; -use futures::Stream; -use http_body::Body; -use pin_project_lite::pin_project; -use tokio::io::{AsyncRead, AsyncReadExt, Take}; -use tokio_util::io::ReaderStream; - -// This code was taken from: https://github.com/tower-rs/tower-http/blob/e8eb54966604ea7fa574a2a25e55232f5cfe675b/tower-http/src/services/fs/mod.rs#L30 -pin_project! { - // NOTE: This could potentially be upstreamed to `http-body`. - /// Adapter that turns an [`impl AsyncRead`][tokio::io::AsyncRead] to an [`impl Body`][http_body::Body]. - #[derive(Debug)] - pub struct AsyncReadBody { - #[pin] - reader: ReaderStream, - } -} - -impl AsyncReadBody -where - T: AsyncRead, -{ - pub(crate) fn with_capacity_limited( - read: T, - capacity: usize, - max_read_bytes: u64, - ) -> AsyncReadBody> { - AsyncReadBody { - reader: ReaderStream::with_capacity(read.take(max_read_bytes), capacity), - } - } -} - -impl Body for AsyncReadBody -where - T: AsyncRead, -{ - type Data = Bytes; - type Error = io::Error; - - fn poll_data( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - self.project().reader.poll_next(cx) - } - - fn poll_trailers( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll, Self::Error>> { - Poll::Ready(Ok(None)) - } -} diff --git a/core/src/custom_uri/mod.rs b/core/src/custom_uri/mod.rs index 33af7c727..89bd85122 100644 --- a/core/src/custom_uri/mod.rs +++ b/core/src/custom_uri/mod.rs @@ -29,7 +29,7 @@ use std::{ use async_stream::stream; use axum::{ - body::{self, Body, BoxBody, Full, StreamBody}, + body::Body, extract::{self, State}, http::{HeaderMap, HeaderValue, Request, Response, StatusCode}, middleware, @@ -38,8 +38,8 @@ use axum::{ Router, }; use bytes::Bytes; -use http_body::combinators::UnsyncBoxBody; use hyper::{header, upgrade::OnUpgrade}; +use hyper_util::rt::TokioIo; use mini_moka::sync::Cache; use tokio::{ fs::{self, File}, @@ -50,7 +50,6 @@ use uuid::Uuid; use self::{serve_file::serve_file, utils::*}; -mod async_read_body; mod mpsc_to_async_write; mod serve_file; mod utils; @@ -97,7 +96,7 @@ async fn request_to_remote_node( p2p: Arc, identity: RemoteIdentity, mut request: Request, -) -> Response> { +) -> Response { let request_upgrade_header = request.headers().get(header::UPGRADE).cloned(); let maybe_client_upgrade = request.extensions_mut().remove::(); @@ -121,17 +120,20 @@ async fn request_to_remote_node( }; tokio::spawn(async move { - let Ok(mut request_upgraded) = request_upgraded.await.map_err(|e| { + let Ok(request_upgraded) = request_upgraded.await.map_err(|e| { warn!(?e, "Error upgrading websocket request;"); }) else { return; }; - let Ok(mut response_upgraded) = response_upgraded.await.map_err(|e| { + let Ok(response_upgraded) = response_upgraded.await.map_err(|e| { warn!(?e, "Error upgrading websocket response;"); }) else { return; }; + let mut request_upgraded = TokioIo::new(request_upgraded); + let mut response_upgraded = TokioIo::new(response_upgraded); + copy_bidirectional(&mut request_upgraded, &mut response_upgraded) .await .map_err(|e| { @@ -147,7 +149,7 @@ async fn request_to_remote_node( async fn get_or_init_lru_entry( state: &LocalState, extract::Path((lib_id, loc_id, path_id)): ExtractedPath, -) -> Result<(CacheValue, Arc), Response> { +) -> Result<(CacheValue, Arc), Response> { let library_id = Uuid::from_str(&lib_id).map_err(bad_request)?; let location_id = loc_id.parse::().map_err(bad_request)?; let file_path_id = path_id @@ -245,7 +247,7 @@ pub fn base_router() -> Router { } else { StatusCode::INTERNAL_SERVER_ERROR }) - .body(body::boxed(Full::from(""))) + .body(Body::from("")) })?; let metadata = file.metadata().await; serve_file( @@ -290,7 +292,7 @@ pub fn base_router() -> Router { } else { StatusCode::INTERNAL_SERVER_ERROR }) - .body(body::boxed(Full::from(""))) + .body(Body::from("")) })?; let resp = InfallibleResponse::builder().header( @@ -335,11 +337,11 @@ pub fn base_router() -> Router { // TODO: Content Type Ok(InfallibleResponse::builder().status(StatusCode::OK).body( - body::boxed(StreamBody::new(stream! { + Body::from_stream(stream! { while let Some(item) = rx.recv().await { yield item; } - })), + }), )) } } @@ -364,7 +366,7 @@ pub fn base_router() -> Router { } else { StatusCode::INTERNAL_SERVER_ERROR }) - .body(body::boxed(Full::from(""))) + .body(Body::from("")) })?; let resp = InfallibleResponse::builder().header( @@ -453,7 +455,7 @@ async fn infer_the_mime_type( ext: &str, file: &mut File, metadata: &Metadata, -) -> Result> { +) -> Result> { let ext = ext.to_lowercase(); let mime_type = match ext.as_str() { // AAC audio diff --git a/core/src/custom_uri/serve_file.rs b/core/src/custom_uri/serve_file.rs index fd80a69e5..460684cf5 100644 --- a/core/src/custom_uri/serve_file.rs +++ b/core/src/custom_uri/serve_file.rs @@ -3,18 +3,18 @@ use crate::util::InfallibleResponse; use std::{fs::Metadata, time::UNIX_EPOCH}; use axum::{ - body::{self, BoxBody, Full, StreamBody}, + body::Body, http::{header, request, HeaderValue, Method, Response, StatusCode}, }; use http_range::HttpRange; use tokio::{ fs::File, - io::{self, AsyncSeekExt, SeekFrom}, + io::{self, AsyncReadExt, AsyncSeekExt, SeekFrom}, }; use tokio_util::io::ReaderStream; use tracing::error; -use super::{async_read_body::AsyncReadBody, utils::*}; +use super::utils::*; // default capacity 64KiB const DEFAULT_CAPACITY: usize = 65536; @@ -31,7 +31,7 @@ pub(crate) async fn serve_file( metadata: io::Result, req: request::Parts, mut resp: InfallibleResponse, -) -> Result, Response> { +) -> Result, Response> { if let Ok(metadata) = metadata { // We only accept range queries if `files.metadata() == Ok(_)` // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges @@ -48,7 +48,7 @@ pub(crate) async fn serve_file( return Ok(resp .status(StatusCode::OK) .header("Content-Length", HeaderValue::from_static("0")) - .body(body::boxed(Full::from("")))); + .body(Body::from(""))); } // ETag @@ -73,9 +73,7 @@ pub(crate) async fn serve_file( // Used for normal requests if let Some(etag) = req.headers.get("If-None-Match") { if etag.as_bytes() == etag_header.as_bytes() { - return Ok(resp - .status(StatusCode::NOT_MODIFIED) - .body(body::boxed(Full::from("")))); + return Ok(resp.status(StatusCode::NOT_MODIFIED).body(Body::from(""))); } } @@ -104,7 +102,7 @@ pub(crate) async fn serve_file( .map_err(internal_server_error)?, ) .status(StatusCode::RANGE_NOT_SATISFIABLE) - .body(body::boxed(Full::from("")))); + .body(Body::from(""))); } let range = ranges.first().expect("checked above"); @@ -116,7 +114,7 @@ pub(crate) async fn serve_file( .map_err(internal_server_error)?, ) .status(StatusCode::RANGE_NOT_SATISFIABLE) - .body(body::boxed(Full::from("")))); + .body(Body::from(""))); } file.seek(SeekFrom::Start(range.start)) @@ -140,14 +138,13 @@ pub(crate) async fn serve_file( HeaderValue::from_str(&range.length.to_string()) .map_err(internal_server_error)?, ) - .body(body::boxed(AsyncReadBody::with_capacity_limited( - file, + .body(Body::from_stream(ReaderStream::with_capacity( + file.take(range.length), DEFAULT_CAPACITY, - range.length, )))); } } } - Ok(resp.body(body::boxed(StreamBody::new(ReaderStream::new(file))))) + Ok(resp.body(Body::from_stream(ReaderStream::new(file)))) } diff --git a/core/src/custom_uri/utils.rs b/core/src/custom_uri/utils.rs index 645da5106..cb54b815f 100644 --- a/core/src/custom_uri/utils.rs +++ b/core/src/custom_uri/utils.rs @@ -3,50 +3,49 @@ use crate::util::InfallibleResponse; use std::{fmt::Debug, panic::Location}; use axum::{ - body::{self, BoxBody}, + body::Body, http::{self, HeaderValue, Method, Request, Response, StatusCode}, middleware::Next, }; -use http_body::Full; use tracing::debug; #[track_caller] -pub(crate) fn bad_request(e: impl Debug) -> http::Response { +pub(crate) fn bad_request(e: impl Debug) -> http::Response { debug!(caller = %Location::caller(), ?e, "400: Bad Request;"); InfallibleResponse::builder() .status(StatusCode::BAD_REQUEST) - .body(body::boxed(Full::from(""))) + .body(Body::from("")) } #[track_caller] -pub(crate) fn not_found(e: impl Debug) -> http::Response { +pub(crate) fn not_found(e: impl Debug) -> http::Response { debug!(caller = %Location::caller(), ?e, "404: Not Found;"); InfallibleResponse::builder() .status(StatusCode::NOT_FOUND) - .body(body::boxed(Full::from(""))) + .body(Body::from("")) } #[track_caller] -pub(crate) fn internal_server_error(e: impl Debug) -> http::Response { +pub(crate) fn internal_server_error(e: impl Debug) -> http::Response { debug!(caller = %Location::caller(), ?e, "500: Internal Server Error;"); InfallibleResponse::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(body::boxed(Full::from(""))) + .body(Body::from("")) } #[track_caller] -pub(crate) fn not_implemented(e: impl Debug) -> http::Response { +pub(crate) fn not_implemented(e: impl Debug) -> http::Response { debug!(caller = %Location::caller(), ?e, "501: Not Implemented;"); InfallibleResponse::builder() .status(StatusCode::NOT_IMPLEMENTED) - .body(body::boxed(Full::from(""))) + .body(Body::from("")) } -pub(crate) async fn cors_middleware(req: Request, next: Next) -> Response { +pub(crate) async fn cors_middleware(req: Request, next: Next) -> Response { if req.method() == Method::OPTIONS { return Response::builder() .header("Access-Control-Allow-Methods", "GET, HEAD, POST, OPTIONS") @@ -54,7 +53,7 @@ pub(crate) async fn cors_middleware(req: Request, next: Next) -> Respon .header("Access-Control-Allow-Headers", "*") .header("Access-Control-Max-Age", "86400") .status(StatusCode::OK) - .body(body::boxed(Full::from(""))) + .body(Body::from("")) .expect("Invalid static response!"); } diff --git a/core/src/p2p/operations/rspc.rs b/core/src/p2p/operations/rspc.rs index ed86c0912..e5e0b3cc8 100644 --- a/core/src/p2p/operations/rspc.rs +++ b/core/src/p2p/operations/rspc.rs @@ -1,9 +1,11 @@ use std::{error::Error, sync::Arc}; -use axum::{body::Body, http, Router}; -use hyper::{server::conn::Http, Response}; +use axum::{extract::Request, http, Router}; +use hyper::{body::Incoming, client::conn::http1::handshake, server::conn::http1, Response}; +use hyper_util::rt::TokioIo; use sd_p2p::{RemoteIdentity, UnicastStream, P2P}; use tokio::io::AsyncWriteExt; +use tower_service::Service; use tracing::debug; use crate::{p2p::Header, Node}; @@ -13,7 +15,7 @@ pub async fn remote_rspc( p2p: Arc, identity: RemoteIdentity, request: http::Request, -) -> Result, Box> { +) -> Result, Box> { let peer = p2p .peers() .get(&identity) @@ -23,7 +25,7 @@ pub async fn remote_rspc( stream.write_all(&Header::RspcRemote.to_bytes()).await?; - let (mut sender, conn) = hyper::client::conn::handshake(stream).await?; + let (mut sender, conn) = handshake(TokioIo::new(stream)).await?; tokio::task::spawn(async move { if let Err(e) = conn.await { println!("Connection error: {:?}", e); @@ -49,10 +51,12 @@ pub(crate) async fn receiver( todo!("No way buddy!"); } - Http::new() - .http1_only(true) - .http1_keep_alive(true) - .serve_connection(stream, service) + let hyper_service = + hyper::service::service_fn(move |request: Request| service.clone().call(request)); + + http1::Builder::new() + .keep_alive(true) + .serve_connection(TokioIo::new(stream), hyper_service) .with_upgrades() .await .map_err(Into::into) diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index e24bd3c0c..b521ee93d 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -38,7 +38,7 @@ uuid = { workspace = true, features = ["serde", "v4"] } # Specific AI dependencies # Note: half and ndarray version must be the same as used in ort -half = { version = "2.1", features = ['num-traits'] } +half = { version = "2.4", features = ['num-traits'] } ndarray = "0.15" url = '2.5' diff --git a/crates/ffmpeg/src/thumbnailer.rs b/crates/ffmpeg/src/thumbnailer.rs index afd008813..6333e34c4 100644 --- a/crates/ffmpeg/src/thumbnailer.rs +++ b/crates/ffmpeg/src/thumbnailer.rs @@ -4,7 +4,7 @@ use std::{io, ops::Deref, path::Path}; use image::{imageops, DynamicImage, RgbImage}; use sd_utils::error::FileIOError; -use tokio::{fs, task::spawn_blocking}; +use tokio::{fs, io::AsyncWriteExt, task::spawn_blocking}; use tracing::error; use webp::Encoder; @@ -37,12 +37,18 @@ impl Thumbnailer { .await .map_err(|e| FileIOError::from((path, e)))?; - fs::write( - output_thumbnail_path, - &*self.process_to_webp_bytes(video_file_path).await?, - ) - .await - .map_err(|e| FileIOError::from((output_thumbnail_path, e)).into()) + let webp = self.process_to_webp_bytes(video_file_path).await?; + let mut file = fs::File::create(output_thumbnail_path) + .await + .map_err(|e: io::Error| FileIOError::from((output_thumbnail_path, e)))?; + + file.write_all(&webp) + .await + .map_err(|e| FileIOError::from((output_thumbnail_path, e)))?; + + file.sync_all() + .await + .map_err(|e| FileIOError::from((output_thumbnail_path, e)).into()) } /// Processes an video input file and returns a webp encoded thumbnail as bytes diff --git a/crates/p2p/Cargo.toml b/crates/p2p/Cargo.toml index 14d21c20b..4727ec7f9 100644 --- a/crates/p2p/Cargo.toml +++ b/crates/p2p/Cargo.toml @@ -31,11 +31,11 @@ uuid = { workspace = true, features = ["serde"] } # Specific P2P dependencies dns-lookup = "2.0" -flume = "=0.11.0" # Must match version used by `mdns-sd` +flume = "=0.11.1" # Must match version used by `mdns-sd` hash_map_diff = "0.2.0" if-watch = { version = "=3.2.0", features = ["tokio"] } # Override features used by libp2p-quic -libp2p-stream = "=0.1.0-alpha" # Update blocked due to custom patch -mdns-sd = "0.11.1" +libp2p-stream = "=0.2.0-alpha" # Update blocked due to custom patch +mdns-sd = "0.11.5" rand_core = "0.6.4" stable-vec = "0.4.1" sync_wrapper = "1.0" @@ -43,7 +43,7 @@ zeroize = { version = "1.8", features = ["derive"] } [dependencies.libp2p] features = ["autonat", "dcutr", "macros", "noise", "quic", "relay", "serde", "tokio", "yamux"] -version = "=0.53.2" # Update blocked due to custom patch +version = "=0.54.1" # Update blocked due to custom patch [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/prisma-cli/Cargo.toml b/crates/prisma-cli/Cargo.toml index 37c764459..c5c0fb669 100644 --- a/crates/prisma-cli/Cargo.toml +++ b/crates/prisma-cli/Cargo.toml @@ -14,5 +14,5 @@ sd-sync-generator = { path = "../sync-generator" } [dependencies.prisma-client-rust-generator] default-features = false features = ["migrations", "specta", "sqlite", "sqlite-create-many"] -git = "https://github.com/brendonovich/prisma-client-rust" -rev = "4f9ef9d38c" +git = "https://github.com/spacedriveapp/prisma-client-rust" +rev = "b22ad7dc7d" diff --git a/crates/sync/example/Cargo.toml b/crates/sync/example/Cargo.toml deleted file mode 100644 index 6893daee8..000000000 --- a/crates/sync/example/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "sd-sync-example" -version = "0.1.0" - -edition.workspace = true -license.workspace = true -publish = false -repository.workspace = true -rust-version.workspace = true - -[dependencies] -# Spacedrive Sub-crates -sd-sync = { path = ".." } - -# Workspace dependencies -axum = { workspace = true } -http = { workspace = true } -prisma-client-rust = { workspace = true } -rspc = { workspace = true, features = ["axum"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["full"] } -uuid = { workspace = true, features = ["v4"] } - -# Specific Core dependencies -dotenv = "0.15.0" -tower-http = { version = "0.4.4", features = ["cors"] } # Update blocked by http diff --git a/crates/sync/example/README.md b/crates/sync/example/README.md deleted file mode 100644 index c3adc9cab..000000000 --- a/crates/sync/example/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Create rspc app - -This app was scaffolded using the [create-rspc-app](https://rspc.dev) CLI. - -## Usage - -```bash -# Terminal One -cd web -pnpm i -pnpm dev - -# Terminal Two -cd api/ -cargo prisma generate -cargo prisma db push -cargo run -``` diff --git a/crates/sync/example/prisma/migrations/.gitkeep b/crates/sync/example/prisma/migrations/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/crates/sync/example/prisma/schema.prisma b/crates/sync/example/prisma/schema.prisma deleted file mode 100644 index efc4ee911..000000000 --- a/crates/sync/example/prisma/schema.prisma +++ /dev/null @@ -1,34 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -datasource db { - provider = "sqlite" - url = "file:dev.db" -} - -generator client { - provider = "cargo prisma" - output = "../src/prisma.rs" -} - -generator sync { - provider = "cargo run -p prisma-cli --bin sync --" - output = "../src/prisma_sync.rs" -} - -/// @owned -model FilePath { - id Bytes @id - path String - - object Object? @relation(fields: [object_id], references: [id]) - object_id Bytes? -} - -/// @shared -model Object { - id Bytes @id - name String - - paths FilePath[] @relation() -} diff --git a/crates/sync/example/src/api/mod.rs b/crates/sync/example/src/api/mod.rs deleted file mode 100644 index 14cc6d257..000000000 --- a/crates/sync/example/src/api/mod.rs +++ /dev/null @@ -1,175 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use rspc::*; -use sd_sync::*; -use serde_json::*; -use std::path::PathBuf; -use tokio::sync::Mutex; -use uuid::Uuid; - -use crate::prisma::{file_path, PrismaClient}; - -pub struct Ctx { - pub dbs: HashMap, - pub prisma: PrismaClient, -} - -type Router = rspc::Router>>; - -fn to_map(v: &impl serde::Serialize) -> serde_json::Map { - match to_value(v).unwrap() { - Value::Object(m) => m, - _ => unreachable!(), - } -} - -pub(crate) fn new() -> RouterBuilder>> { - Router::new() - .config(Config::new().export_ts_bindings( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("web/src/utils/bindings.ts"), - )) - .mutation("testCreate", |r| { - r(|ctx, _: String| async move { - let prisma = &ctx.lock().await.prisma; - - let res = prisma - .file_path() - .create(vec![], String::new(), vec![]) - .exec_raw() - .await - .unwrap(); - - file_path::Create::operation_from_data(&res); - - Ok(()) - }) - }) - .mutation("createDatabase", |r| { - r(|ctx, _: String| async move { - let dbs = &mut ctx.lock().await.dbs; - let uuid = Uuid::new_v4(); - - dbs.insert(uuid, Db::new(uuid)); - - let ids = dbs.keys().copied().collect::>(); - - for db in dbs.values_mut() { - for id in &ids { - db.register_node(*id); - } - } - - Ok(uuid) - }) - }) - .mutation("removeDatabases", |r| { - r(|ctx, _: String| async move { - let dbs = &mut ctx.lock().await.dbs; - - dbs.drain(); - - Ok(()) - }) - }) - .query("dbs", |r| { - r(|ctx, _: String| async move { - let dbs = &mut ctx.lock().await.dbs; - - Ok(dbs.iter().map(|(id, _)| *id).collect::>()) - }) - }) - .query("db.tags", |r| { - r(|ctx, id: String| async move { - let dbs = &mut ctx.lock().await.dbs; - - let id = id.parse().unwrap(); - - Ok(dbs.get(&id).unwrap().tags.clone()) - }) - }) - .query("file_path.list", |r| { - r(|ctx, id: String| async move { - let dbs = &mut ctx.lock().await.dbs; - - let db = dbs.get(&id.parse().unwrap()).unwrap(); - - let file_paths = db.file_paths.values().map(Clone::clone).collect::>(); - - Ok(file_paths) - }) - }) - .mutation("file_path.create", |r| { - r(|ctx, db: String| async move { - let dbs = &mut ctx.lock().await.dbs; - - let db = dbs.get_mut(&db.parse().unwrap()).unwrap(); - - let id = Uuid::new_v4(); - - let file_path = FilePath { - id, - path: String::new(), - file: None, - }; - - let op = db.create_crdt_operation(CRDTOperationType::Owned(OwnedOperation { - model: "FilePath".to_string(), - items: vec![OwnedOperationItem { - id: serde_json::to_value(id).unwrap(), - data: OwnedOperationData::Create( - serde_json::from_value(serde_json::to_value(&file_path).unwrap()) - .unwrap(), - ), - }], - })); - - db.receive_crdt_operations(vec![op]); - - file_path - }) - }) - .query("message.list", |r| { - r(|ctx, id: String| async move { - let dbs = &mut ctx.lock().await.dbs; - - let db = dbs.get(&id.parse().unwrap()).unwrap(); - - Ok(db._operations.clone()) - }) - }) - .mutation("pullOperations", |r| { - r(|ctx, db_id: String| async move { - let dbs = &mut ctx.lock().await.dbs; - - let db_id = db_id.parse().unwrap(); - - let ops = dbs.values().flat_map(|db| db._operations.clone()).collect(); - - let db = dbs.get_mut(&db_id).unwrap(); - - db.receive_crdt_operations(ops); - - Ok(()) - }) - }) - .query("operations", |r| { - r(|ctx, _: String| async move { - let dbs = &mut ctx.lock().await.dbs; - - let mut hashmap = HashMap::new(); - - for db in dbs.values_mut() { - for op in &db._operations { - hashmap.insert(op.id, op.clone()); - } - } - - let mut array = hashmap.into_values().collect::>(); - - array.sort_by(|a, b| a.id.partial_cmp(&b.id).unwrap()); - - Ok(array) - }) - }) -} diff --git a/crates/sync/example/src/main.rs b/crates/sync/example/src/main.rs deleted file mode 100644 index a04eae216..000000000 --- a/crates/sync/example/src/main.rs +++ /dev/null @@ -1,45 +0,0 @@ -use api::Ctx; -use axum::{ - http::{HeaderValue, Method}, - routing::get, -}; -use std::{net::SocketAddr, sync::Arc}; -use tokio::sync::Mutex; -use tower_http::cors::CorsLayer; - -mod api; -mod prisma; -mod prisma_sync; -mod utils; - -async fn router() -> axum::Router { - let router = api::new().build().arced(); - - let ctx = Arc::new(Mutex::new(Ctx { - dbs: Default::default(), - prisma: prisma::new_client().await.unwrap(), - })); - - axum::Router::new() - .route("/", get(|| async { "Hello 'rspc'!" })) - .route("/rspc/:id", router.endpoint(move || ctx.clone()).axum()) - .layer( - CorsLayer::new() - .allow_origin("http://localhost:3000".parse::().unwrap()) - .allow_headers(vec![http::header::CONTENT_TYPE]) - .allow_methods([Method::GET, Method::POST]), - ) -} - -#[tokio::main] -async fn main() { - dotenv::dotenv().ok(); - - let addr = "[::]:9000".parse::().unwrap(); // This listens on IPv6 and IPv4 - println!("{} listening on http://{}", env!("CARGO_CRATE_NAME"), addr); - axum::Server::bind(&addr) - .serve(router().await.into_make_service()) - .with_graceful_shutdown(utils::axum_shutdown_signal()) - .await - .expect("Error with HTTP server!"); -} diff --git a/crates/sync/example/src/utils.rs b/crates/sync/example/src/utils.rs deleted file mode 100644 index f6437d365..000000000 --- a/crates/sync/example/src/utils.rs +++ /dev/null @@ -1,28 +0,0 @@ -use tokio::signal; - -/// shutdown_signal will inform axum to gracefully shutdown when the process is asked to shutdown. -pub async fn axum_shutdown_signal() { - let ctrl_c = async { - signal::ctrl_c() - .await - .expect("failed to install Ctrl+C handler"); - }; - - #[cfg(unix)] - let terminate = async { - signal::unix::signal(signal::unix::SignalKind::terminate()) - .expect("failed to install signal handler") - .recv() - .await; - }; - - #[cfg(not(unix))] - let terminate = std::future::pending::<()>(); - - tokio::select! { - _ = ctrl_c => {}, - _ = terminate => {}, - } - - println!("signal received, starting graceful shutdown"); -} diff --git a/crates/sync/example/web/.gitignore b/crates/sync/example/web/.gitignore deleted file mode 100644 index 76add878f..000000000 --- a/crates/sync/example/web/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist \ No newline at end of file diff --git a/crates/sync/example/web/README.md b/crates/sync/example/web/README.md deleted file mode 100644 index 434f7bb9d..000000000 --- a/crates/sync/example/web/README.md +++ /dev/null @@ -1,34 +0,0 @@ -## Usage - -Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`. - -This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template. - -```bash -$ npm install # or pnpm install or yarn install -``` - -### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) - -## Available Scripts - -In the project directory, you can run: - -### `npm dev` or `npm start` - -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.
- -### `npm run build` - -Builds the app for production to the `dist` folder.
-It correctly bundles Solid in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -## Deployment - -You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.) diff --git a/crates/sync/example/web/index.html b/crates/sync/example/web/index.html deleted file mode 100644 index f22a9d4f1..000000000 --- a/crates/sync/example/web/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - Solid App - - - -
- - - - diff --git a/crates/sync/example/web/package.json b/crates/sync/example/web/package.json deleted file mode 100644 index 0c95d0ba5..000000000 --- a/crates/sync/example/web/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "example-2", - "version": "0.0.0", - "description": "", - "scripts": { - "dev": "vite", - "build": "vite build", - "serve": "vite preview", - "typecheck": "tsc --noEmit" - }, - "license": "MIT", - "dependencies": { - "clsx": "^2.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "solid-js": "^1.8.3" - }, - "devDependencies": { - "@tanstack/react-query": "^4.36.1", - "typescript": "^5.6.2", - "vite": "^5.2.0", - "tailwindcss": "^3.3.3" - } -} diff --git a/crates/sync/example/web/postcss.config.js b/crates/sync/example/web/postcss.config.js deleted file mode 100644 index 054c147cb..000000000 --- a/crates/sync/example/web/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {} - } -}; diff --git a/crates/sync/example/web/src/App.tsx b/crates/sync/example/web/src/App.tsx deleted file mode 100644 index 6c90cbd4b..000000000 --- a/crates/sync/example/web/src/App.tsx +++ /dev/null @@ -1,172 +0,0 @@ -// import clsx from 'clsx'; -// import { Suspense, useState } from 'react'; -// import { tests } from './test'; -// import { CRDTOperationType, rspc } from './utils/rspc'; - -// export function App() { -// const dbs = rspc.useQuery(['dbs', 'cringe']); - -// const operations = rspc.useQuery(['operations', 'cringe']); - -// const createDb = rspc.useMutation('createDatabase'); -// const removeDbs = rspc.useMutation('removeDatabases'); -// const testCreate = rspc.useMutation('testCreate'); - -// return ( -//
-//
-//
-// -// -// -//
-//
    -// {Object.entries(tests).map(([key, test]) => ( -//
  • -// -//
  • -// ))} -//
-//
-//
-//
    -// {dbs.data?.map((id) => ( -// -// -// -// ))} -//
-//
-//
-//

All Operations

-//
    -// {operations.data?.map((op) => ( -//
  • -//

    ID: {op.id}

    -//

    Timestamp: {op.timestamp.toString()}

    -//

    Node: {op.node}

    -//
  • -// ))} -//
-//
-//
-// ); -// } - -// interface DatabaseViewProps { -// id: string; -// } -// const TABS = ['File Paths', 'Objects', 'Tags', 'Operations']; - -// function DatabaseView(props: DatabaseViewProps) { -// const [currentTab, setCurrentTab] = useState<(typeof TABS)[number]>('Operations'); - -// const pullOperations = rspc.useMutation('pullOperations'); - -// return ( -//
-//
-//

{props.id}

-// -//
-//
-// -// -// {currentTab === 'File Paths' && } -// {currentTab === 'Operations' && } -// -//
-//
-// ); -// } - -// function FilePathList(props: { db: string }) { -// const createFilePath = rspc.useMutation('file_path.create'); -// const filePaths = rspc.useQuery(['file_path.list', props.db]); - -// return ( -//
-// {filePaths.data && ( -//
    -// {filePaths.data -// .sort((a, b) => a.id.localeCompare(b.id)) -// .map((path) => ( -//
  • {JSON.stringify(path)}
  • -// ))} -//
-// )} -// -//
-// ); -// } - -// function messageType(msg: CRDTOperationType) { -// if ('items' in msg) { -// return 'Owned'; -// } else if ('record_id' in msg) { -// return 'Shared'; -// } -// } - -// function OperationList(props: { db: string }) { -// const messages = rspc.useQuery(['message.list', props.db]); - -// return ( -//
-// {messages.data && ( -// -// {messages.data -// .sort((a, b) => Number(a.timestamp - b.timestamp)) -// .map((message) => ( -// -// -// -// -// -// ))} -//
{message.id} -// {new Date( -// Number(message.timestamp) / 10000000 -// ).toLocaleTimeString()} -// -// {messageType(message.typ)} -//
-// )} -//
-// ); -// } - -// const ButtonStyles = 'bg-blue-500 text-white px-2 py-1 rounded-md'; - -export {}; diff --git a/crates/sync/example/web/src/index.css b/crates/sync/example/web/src/index.css deleted file mode 100644 index b5c61c956..000000000 --- a/crates/sync/example/web/src/index.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/crates/sync/example/web/src/index.tsx b/crates/sync/example/web/src/index.tsx deleted file mode 100644 index 6c9a09bac..000000000 --- a/crates/sync/example/web/src/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// /* @refresh reload */ -// import { Suspense } from 'react'; -// import { createRoot } from 'react-dom/client'; -// import { App } from './App'; -// import './index.css'; -// import { queryClient, rspc, rspcClient } from './utils/rspc'; - -// createRoot(document.getElementById('root') as HTMLElement).render( -// -// -// -// -// -// ); - -export {}; diff --git a/crates/sync/example/web/src/test.ts b/crates/sync/example/web/src/test.ts deleted file mode 100644 index e516517cc..000000000 --- a/crates/sync/example/web/src/test.ts +++ /dev/null @@ -1,47 +0,0 @@ -// import { queryClient, rspcClient } from './utils/rspc'; - -// function test(fn: () => Promise) { -// return async () => { -// await fn(); -// queryClient.invalidateQueries(); -// }; -// } - -// const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)); - -// export const tests = { -// three: { -// name: 'Three', -// run: test(async () => { -// const [db1, db2, db3] = await Promise.all([ -// rspcClient.mutation(['createDatabase', ' ']), -// rspcClient.mutation(['createDatabase', ' ']), -// rspcClient.mutation(['createDatabase', ' ']) -// ]); - -// const dbs = await rspcClient.query(['dbs', 'cringe']); - -// for (const db of dbs) { -// await rspcClient.mutation(['file_path.create', db]); -// } - -// for (const db of dbs) { -// await rspcClient.mutation(['pullOperations', db]); -// } - -// await rspcClient.mutation(['file_path.create', dbs[0]]); -// await rspcClient.mutation(['file_path.create', dbs[0]]); - -// for (const db of dbs) { -// await rspcClient.mutation(['pullOperations', db]); -// } - -// await rspcClient.mutation(['pullOperations', dbs[1]]); -// await rspcClient.mutation(['pullOperations', dbs[1]]); -// await rspcClient.mutation(['pullOperations', dbs[1]]); -// await rspcClient.mutation(['pullOperations', dbs[1]]); -// }) -// } -// }; - -export {}; diff --git a/crates/sync/example/web/src/utils/bindings.ts b/crates/sync/example/web/src/utils/bindings.ts deleted file mode 100644 index 3d31e95bb..000000000 --- a/crates/sync/example/web/src/utils/bindings.ts +++ /dev/null @@ -1,80 +0,0 @@ -// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually. - -export type Procedures = { - queries: - | { key: 'db.tags'; input: string; result: Record } - | { key: 'dbs'; input: string; result: Array } - | { key: 'file_path.list'; input: string; result: Array } - | { key: 'message.list'; input: string; result: Array } - | { key: 'operations'; input: string; result: Array }; - mutations: - | { key: 'createDatabase'; input: string; result: string } - | { key: 'file_path.create'; input: string; result: FilePath } - | { key: 'pullOperations'; input: string; result: null } - | { key: 'removeDatabases'; input: string; result: null } - | { key: 'testCreate'; input: string; result: null }; - subscriptions: never; -}; - -export interface CRDTOperation { - node: string; - timestamp: bigint; - id: string; - typ: CRDTOperationType; -} - -export type CRDTOperationType = SharedOperation | RelationOperation | OwnedOperation; - -export interface Color { - red: number; - green: number; - blue: number; -} - -export interface FilePath { - id: string; - path: string; - file: string | null; -} - -export interface OwnedOperation { - model: string; - items: Array; -} - -export type OwnedOperationData = - | { Create: Record } - | { Update: Record } - | 'Delete'; - -export interface OwnedOperationItem { - id: any; - data: OwnedOperationData; -} - -export interface RelationOperation { - relation_item: string; - relation_group: string; - relation: string; - data: RelationOperationData; -} - -export type RelationOperationData = 'Create' | { Update: { field: string; value: any } } | 'Delete'; - -export interface SharedOperation { - record_id: string; - model: string; - data: SharedOperationData; -} - -export type SharedOperationCreateData = { Unique: Record } | 'Atomic'; - -export type SharedOperationData = - | { Create: SharedOperationCreateData } - | { Update: { field: string; value: any } } - | 'Delete'; - -export interface Tag { - color: Color; - name: string; -} diff --git a/crates/sync/example/web/src/utils/rspc.ts b/crates/sync/example/web/src/utils/rspc.ts deleted file mode 100644 index 9991bd5f0..000000000 --- a/crates/sync/example/web/src/utils/rspc.ts +++ /dev/null @@ -1,29 +0,0 @@ -// import { createClient, httpLink } from '@oscartbeaumont-sd/rspc-client'; -// import { createReactHooks } from '@oscartbeaumont-sd/rspc-react'; -// import { QueryClient } from '@tanstack/react-query'; -// import type { Procedures } from './bindings'; - -// export * from './bindings'; - -// // These are generated by rspc in Rust for you. - -// const rspc = createReactHooks(); - -// const rspcClient = rspc.createClient({ -// links: [httpLink({ url: 'http://localhost:9000/rspc' })] -// }); - -// const queryClient = new QueryClient({ -// defaultOptions: { -// queries: { -// suspense: true -// }, -// mutations: { -// onSuccess: () => queryClient.invalidateQueries() -// } -// } -// }); - -// export { rspc, rspcClient, queryClient }; - -export {}; diff --git a/crates/sync/example/web/tailwind.config.js b/crates/sync/example/web/tailwind.config.js deleted file mode 100644 index 7cf6cc57b..000000000 --- a/crates/sync/example/web/tailwind.config.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], - theme: { - extend: {} - }, - plugins: [] -}; diff --git a/crates/sync/example/web/tsconfig.json b/crates/sync/example/web/tsconfig.json deleted file mode 100644 index 1d5d18140..000000000 --- a/crates/sync/example/web/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "types": ["vite/client"], - "noEmit": true, - "isolatedModules": true - } -} diff --git a/crates/sync/example/web/vite.config.ts b/crates/sync/example/web/vite.config.ts deleted file mode 100644 index 29f022f92..000000000 --- a/crates/sync/example/web/vite.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [react()], - server: { - port: 3000 - }, - build: { - target: 'esnext' - } -}); diff --git a/interface/app/$libraryId/Explorer/ContextMenu/OpenWith.tsx b/interface/app/$libraryId/Explorer/ContextMenu/OpenWith.tsx index 381548a0a..d11ba36bc 100644 --- a/interface/app/$libraryId/Explorer/ContextMenu/OpenWith.tsx +++ b/interface/app/$libraryId/Explorer/ContextMenu/OpenWith.tsx @@ -81,9 +81,9 @@ const Items = ({ const paths = selectedEphemeralPaths.map((obj) => obj.path); const { t } = useLocale(); - const { data: apps } = useQuery( - ['openWith', ids, paths], - async () => { + const { data: apps } = useQuery({ + queryKey: ['openWith', ids, paths], + queryFn: async () => { const handleError = (res: Result) => { if (res?.status === 'error') { toast.error('Failed to get applications capable to open file'); @@ -104,8 +104,8 @@ const Items = ({ .then((res) => res.flat()) .then((res) => res.sort((a, b) => a.name.localeCompare(b.name))); }, - { initialData: [] } - ); + initialData: [] + }); return ( <> diff --git a/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx b/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx index 1c79d03ac..41a882e0e 100644 --- a/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx +++ b/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx @@ -224,7 +224,7 @@ const SpacedropNodes = () => { { spacedrop.mutateAsync({ identity: id, diff --git a/interface/app/$libraryId/Explorer/ExplorerTagBar.tsx b/interface/app/$libraryId/Explorer/ExplorerTagBar.tsx index aca3ec3f1..2dd8ef46e 100644 --- a/interface/app/$libraryId/Explorer/ExplorerTagBar.tsx +++ b/interface/app/$libraryId/Explorer/ExplorerTagBar.tsx @@ -117,7 +117,7 @@ export const ExplorerTagBar = () => { const { data: allTags = [] } = useLibraryQuery(['tags.list']); const mutation = useLibraryMutation(['tags.assign'], { - onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths']) + onSuccess: () => rspc.queryClient.invalidateQueries({ queryKey: ['search.paths'] }) }); const { t } = useLocale(); diff --git a/interface/app/$libraryId/Explorer/FilePath/DecryptDialog.tsx b/interface/app/$libraryId/Explorer/FilePath/DecryptDialog.tsx deleted file mode 100644 index 0f2cbbc2e..000000000 --- a/interface/app/$libraryId/Explorer/FilePath/DecryptDialog.tsx +++ /dev/null @@ -1,179 +0,0 @@ -// import { RadioGroup } from '@headlessui/react'; -// import { Info } from '@phosphor-icons/react'; -// import { useLibraryMutation, useLibraryQuery } from '@sd/client'; -// import { Button, Dialog, Tooltip, UseDialogProps, useDialog } from '@sd/ui'; -// import { PasswordInput, Switch, useZodForm, z } from '@sd/ui/src/forms'; -// import { showAlertDialog } from '~/components'; -// import { usePlatform } from '~/util/Platform'; - -// const schema = z.object({ -// type: z.union([z.literal('password'), z.literal('key')]), -// outputPath: z.string(), -// mountAssociatedKey: z.boolean(), -// password: z.string(), -// saveToKeyManager: z.boolean() -// }); - -// interface Props extends UseDialogProps { -// location_id: number; -// path_id: number; -// } - -// export default (props: Props) => { -// const platform = usePlatform(); - -// const mountedUuids = useLibraryQuery(['keys.listMounted'], { -// onSuccess: (data) => { -// hasMountedKeys = data.length > 0 ? true : false; -// if (!hasMountedKeys) { -// form.setValue('type', 'password'); -// } else { -// form.setValue('type', 'key'); -// } -// } -// }); - -// let hasMountedKeys = -// mountedUuids.data !== undefined && mountedUuids.data.length > 0 ? true : false; - -// const decryptFile = useLibraryMutation('files.decryptFiles', { -// onSuccess: () => { -// showAlertDialog({ -// title: 'Success', -// value: 'The decryption job has started successfully. You may track the progress in the job overview panel.' -// }); -// }, -// onError: () => { -// showAlertDialog({ -// title: 'Error', -// value: 'The decryption job failed to start.' -// }); -// } -// }); - -// const form = useZodForm({ -// defaultValues: { -// type: hasMountedKeys ? 'key' : 'password', -// saveToKeyManager: true, -// outputPath: '', -// password: '', -// mountAssociatedKey: true -// }, -// schema -// }); - -// return ( -// -// decryptFile.mutateAsync({ -// location_id: props.location_id, -// file_path_ids: [props.path_id], -// output_path: data.outputPath !== '' ? data.outputPath : null, -// mount_associated_key: data.mountAssociatedKey, -// password: data.type === 'password' ? data.password : null, -// save_to_library: data.type === 'password' ? data.saveToKeyManager : null -// }) -// )} -// title="Decrypt a file" -// description="Leave the output file blank for the default." -// loading={decryptFile.isLoading} -// ctaLabel="Decrypt" -// > -//
-//

Key Type

-// form.setValue('type', e)} -// className="mt-2 flex flex-row gap-2" -// > -// -// {({ checked }) => ( -// -// )} -// -// -// {({ checked }) => ( -// -// )} -// -// - -// {form.watch('type') === 'key' && ( -//
-// form.setValue('mountAssociatedKey', e)} -// /> -// -// Automatically mount key -// -// -// -// -//
-// )} - -// {form.watch('type') === 'password' && ( -// <> -// - -//
-// -// -// Save to Key Manager -// -// -// -// -//
-// -// )} - -//

Output file

-// -//
-//
-// ); -// }; diff --git a/interface/app/$libraryId/Explorer/FilePath/DeleteDialog.tsx b/interface/app/$libraryId/Explorer/FilePath/DeleteDialog.tsx index bd9d2fdb0..8ce5a5184 100644 --- a/interface/app/$libraryId/Explorer/FilePath/DeleteDialog.tsx +++ b/interface/app/$libraryId/Explorer/FilePath/DeleteDialog.tsx @@ -110,7 +110,7 @@ export default (props: Props) => { dialog={useDialog(props)} title={t('delete_dialog_title', { prefix, type: translatedType })} description={description} - loading={deleteFile.isLoading} + loading={deleteFile.isPending} ctaLabel={t('delete_forever')} ctaSecondLabel={t('move_to_trash')} closeLabel={t('close')} diff --git a/interface/app/$libraryId/Explorer/FilePath/EncryptDialog.tsx b/interface/app/$libraryId/Explorer/FilePath/EncryptDialog.tsx deleted file mode 100644 index 9daf21a7a..000000000 --- a/interface/app/$libraryId/Explorer/FilePath/EncryptDialog.tsx +++ /dev/null @@ -1,177 +0,0 @@ -// import { -// Algorithm, -// hashingAlgoSlugSchema, -// slugFromHashingAlgo, -// useLibraryMutation, -// useLibraryQuery -// } from '@sd/client'; -// import { Button, Dialog, Select, SelectOption, UseDialogProps, useDialog } from '@sd/ui'; -// import { CheckBox, useZodForm, z } from '@sd/ui/src/forms'; -// import { showAlertDialog } from '~/components'; -// import { usePlatform } from '~/util/Platform'; -// import { KeyListSelectOptions } from '../../KeyManager/List'; - -// interface Props extends UseDialogProps { -// location_id: number; -// path_id: number; -// } - -// const schema = z.object({ -// key: z.string(), -// encryptionAlgo: z.string(), -// hashingAlgo: hashingAlgoSlugSchema, -// metadata: z.boolean(), -// previewMedia: z.boolean(), -// outputPath: z.string() -// }); - -// export default (props: Props) => { -// const platform = usePlatform(); - -// const UpdateKey = (uuid: string) => { -// form.setValue('key', uuid); -// const hashAlg = keys.data?.find((key) => { -// return key.uuid === uuid; -// })?.hashing_algorithm; -// hashAlg && form.setValue('hashingAlgo', slugFromHashingAlgo(hashAlg)); -// }; - -// const keys = useLibraryQuery(['keys.list']); -// const mountedUuids = useLibraryQuery(['keys.listMounted'], { -// onSuccess: (data) => { -// UpdateKey(data[0] ?? ''); -// } -// }); - -// const encryptFile = useLibraryMutation('files.encryptFiles', { -// onSuccess: () => { -// showAlertDialog({ -// title: 'Success', -// value: 'The encryption job has started successfully. You may track the progress in the job overview panel.' -// }); -// }, -// onError: () => { -// showAlertDialog({ -// title: 'Error', -// value: 'The encryption job failed to start.' -// }); -// } -// }); - -// const form = useZodForm({ -// defaultValues: { encryptionAlgo: 'XChaCha20Poly1305', outputPath: '' }, -// schema -// }); - -// return ( -// -// encryptFile.mutateAsync({ -// algorithm: data.encryptionAlgo as Algorithm, -// key_uuid: data.key, -// location_id: props.location_id, -// file_path_ids: [props.path_id], -// metadata: data.metadata, -// preview_media: data.previewMedia -// }) -// )} -// dialog={useDialog(props)} -// title="Encrypt a file" -// description="Configure your encryption settings. Leave the output file blank for the default." -// loading={encryptFile.isLoading} -// ctaLabel="Encrypt" -// > -//
-//
-// Key -// -//
-//
-// Output file - -// -//
-//
- -//
-//
-// Encryption -// -//
-//
-// Hashing -// -//
-//
- -//
-//
-// Metadata -// -//
-//
-// Preview Media -// -//
-//
-//
-// ); -// }; diff --git a/interface/app/$libraryId/Explorer/FilePath/EraseDialog.tsx b/interface/app/$libraryId/Explorer/FilePath/EraseDialog.tsx deleted file mode 100644 index 4bd84d28e..000000000 --- a/interface/app/$libraryId/Explorer/FilePath/EraseDialog.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// import { useState } from 'react'; -// import { FilePath, useLibraryMutation, useZodForm } from '@sd/client'; -// import { Dialog, Slider, useDialog, UseDialogProps, z } from '@sd/ui'; -// import { useLocale } from '~/hooks'; - -// interface Props extends UseDialogProps { -// locationId: number; -// filePaths: FilePath[]; -// } - -// const schema = z.object({ -// passes: z.number() -// }); - -// export default (props: Props) => { -// const { t } = useLocale(); -// const eraseFile = useLibraryMutation('files.eraseFiles'); - -// const form = useZodForm({ -// schema, -// defaultValues: { -// passes: 4 -// } -// }); - -// const [passes, setPasses] = useState([4]); - -// return ( -// -// eraseFile.mutateAsync({ -// location_id: props.locationId, -// file_path_ids: props.filePaths.map((p) => p.id), -// passes: data.passes.toString() -// }) -// )} -// dialog={useDialog(props)} -// title={t('erase_a_file')} -// description={t('erase_a_file_description')} -// loading={eraseFile.isLoading} -// ctaLabel={t('erase')} -// > -//
-// {t('number_of_passes')} - -//
-//
-// { -// setPasses(val); -// form.setValue('passes', val[0] ?? 1); -// }} -// /> -//
-// {passes} -//
-//
- -// {/*

TODO: checkbox for "erase all matching files" (only if a file is selected)

*/} -//
-// ); -// }; diff --git a/interface/app/$libraryId/Explorer/FilePath/ErrorBarrier.tsx b/interface/app/$libraryId/Explorer/FilePath/ErrorBarrier.tsx new file mode 100644 index 000000000..9290ca589 --- /dev/null +++ b/interface/app/$libraryId/Explorer/FilePath/ErrorBarrier.tsx @@ -0,0 +1,40 @@ +import React, { Component, ReactNode } from 'react'; + +interface ErrorBarrierProps { + onError: (error: Error, info: React.ErrorInfo) => void; + children: ReactNode; +} + +interface ErrorBarrierState { + hasError: boolean; +} + +export class ErrorBarrier extends Component { + constructor(props: ErrorBarrierProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error) { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + // Call the onError function passed as a prop + this.props.onError(error, info); + // Reset the error state after calling onError + Promise.resolve().then(() => this.setState({ hasError: false })); + } + + render() { + if (this.state.hasError) { + // Render nothing since the parent component will handle the error + return null; + } + + return this.props.children; + } +} + +export default ErrorBarrier; diff --git a/interface/app/$libraryId/Explorer/FilePath/Image.tsx b/interface/app/$libraryId/Explorer/FilePath/Image.tsx index 5be914315..e7036595b 100644 --- a/interface/app/$libraryId/Explorer/FilePath/Image.tsx +++ b/interface/app/$libraryId/Explorer/FilePath/Image.tsx @@ -1,14 +1,7 @@ import { ComponentProps, forwardRef } from 'react'; -import { useSize } from './utils'; - -export interface ImageProps extends ComponentProps<'img'> { - extension?: string; - size: ReturnType; -} - -export const Image = forwardRef( - ({ crossOrigin, size, ...props }, ref) => ( +export const Image = forwardRef>( + ({ crossOrigin, ...props }, ref) => ( { +interface LayeredFileIconProps extends Omit, 'src'> { kind: ObjectKindKey; + isDir: boolean; extension: string | null; + customIcon: IconTypes | null; } const SUPPORTED_ICONS = ['Document', 'Code', 'Text', 'Config']; @@ -17,8 +20,18 @@ const positionConfig: Record = { }; const LayeredFileIcon = forwardRef( - ({ kind, extension, ...props }, ref) => { - const iconImg = ; + ({ kind, isDir, extension, customIcon, ...props }, ref) => { + const isDark = useIsDark(); + + const src = useMemo( + () => + customIcon + ? getIconByName(customIcon, isDark) + : getIcon(kind, isDark, extension, isDir), + [customIcon, isDark, kind, extension, isDir] + ); + + const iconImg = {`${kind}; if (SUPPORTED_ICONS.includes(kind) === false) { return iconImg; diff --git a/interface/app/$libraryId/Explorer/FilePath/Original.tsx b/interface/app/$libraryId/Explorer/FilePath/Original.tsx index 451b13d35..6b319793e 100644 --- a/interface/app/$libraryId/Explorer/FilePath/Original.tsx +++ b/interface/app/$libraryId/Explorer/FilePath/Original.tsx @@ -8,27 +8,28 @@ import { useState, type VideoHTMLAttributes } from 'react'; -import { getItemFilePath, useLibraryContext } from '@sd/client'; +import { ObjectKindKey, useLibraryContext } from '@sd/client'; import i18n from '~/app/I18n'; import { PDFViewer, TextViewer } from '~/components'; -import { useLocale } from '~/hooks'; +import { useIsDark, useLocale } from '~/hooks'; import { pdfViewerEnabled } from '~/util/pdfViewer'; import { usePlatform } from '~/util/Platform'; import { useExplorerContext } from '../Context'; import { explorerStore } from '../store'; -import { ExplorerItemData } from '../useExplorerItemData'; import { Image } from './Image'; import { useBlackBars, useSize } from './utils'; interface OriginalRendererProps { src: string; - className: string; - frameClassName: string; - itemData: ExplorerItemData; - isDark: boolean; + fileId: number | null; + locationId: number | null; + path: string | null; + className?: string; + frameClassName?: string; + kind: ObjectKindKey; + extension: string | null; childClassName?: string; - size?: number; magnification?: number; mediaControls?: boolean; frame?: boolean; @@ -37,44 +38,53 @@ interface OriginalRendererProps { blackBars?: boolean; blackBarsSize?: number; onLoad?(): void; - onError?(e: ErrorEvent | SyntheticEvent): void; } export function Original({ - itemData, - filePath, + path, + fileId, + locationId, ...props -}: Omit & { - filePath: ReturnType; -}) { - const [error, setError] = useState(false); - if (error) throw new Error('onError'); +}: Omit) { + const [error, setError] = useState(null); + if (error != null) throw error; const Renderer = useMemo(() => { - const kind = originalRendererKind(itemData); + const kind = originalRendererKind(props.kind, props.extension); return ORIGINAL_RENDERERS[kind]; - }, [itemData]); + }, [props.kind, props.extension]); if (!Renderer) throw new Error('no renderer!'); const platform = usePlatform(); const { library } = useLibraryContext(); const { parent } = useExplorerContext(); + locationId = locationId ?? (parent?.type === 'Location' ? parent.location.id : null); const src = useMemo(() => { - const locationId = - itemData.locationId ?? (parent?.type === 'Location' ? parent.location.id : null); - - if (filePath && (itemData.extension !== 'pdf' || pdfViewerEnabled())) { - if ('id' in filePath && locationId) - return platform.getFileUrl(library.uuid, locationId, filePath.id); - else if ('path' in filePath) return platform.getFileUrlByPath(filePath.path); + if (props.extension !== 'pdf' || pdfViewerEnabled()) { + if (fileId != null && locationId) + return platform.getFileUrl(library.uuid, locationId, fileId); + else if (path) return platform.getFileUrlByPath(path); } - }, [itemData, filePath, library.uuid, parent, platform]); + }, [props.extension, fileId, locationId, platform, library.uuid, path]); if (src === undefined) throw new Error('no src!'); - return setError(true)} {...props} />; + return ( + + setError( + ('error' in event && event.error instanceof Error && event.error) || + new Error( + ('message' in event && event.message) || 'Filetype is not supported yet' + ) + ) + } + {...props} + /> + ); } const TEXT_RENDERER: OriginalRenderer = (props) => ( @@ -89,18 +99,20 @@ const TEXT_RENDERER: OriginalRenderer = (props) => ( props.frame && [props.frameClassName, '!bg-none p-2'] )} codeExtension={ - ((props.itemData.kind === 'Code' || props.itemData.kind === 'Config') && - props.itemData.extension) || - '' + ((props.kind === 'Code' || props.kind === 'Config') && props.extension) || '' } isSidebarPreview={props.isSidebarPreview} /> ); -type OriginalRenderer = (props: OriginalRendererProps) => JSX.Element; +type OriginalRenderer = ( + props: Omit & { + onError?(e: ErrorEvent | SyntheticEvent): void; + } +) => JSX.Element; -function originalRendererKind(itemData: ExplorerItemData) { - return itemData.extension === 'pdf' ? 'PDF' : itemData.kind; +function originalRendererKind(kind: ObjectKindKey, extension: string | null) { + return extension === 'pdf' ? 'PDF' : kind; } type OriginalRendererKind = ReturnType; @@ -135,44 +147,45 @@ const ORIGINAL_RENDERERS: { )} /> ), - Audio: (props) => ( - <> - - {props.mediaControls && ( - - )} - - ), + Audio: (props) => { + const isDark = useIsDark(); + return ( + <> + + {props.mediaControls && ( + + )} + + ); + }, Image: (props) => { const ref = useRef(null); - const size = useSize(ref); return (
{ + cover?: boolean; + blackBars?: boolean; + blackBarsSize?: number; + videoExtension?: string; +} + +const Thumbnail = memo( + forwardRef( + ( + { + blackBars, + blackBarsSize, + videoExtension: extension, + cover, + className, + style, + ...props + }, + _ref + ) => { + const ref = useRef(null); + useImperativeHandle( + _ref, + () => ref.current + ); + + const size = useSize(ref); + + const { style: blackBarsStyle } = useBlackBars(ref, size, { + size: blackBarsSize, + disabled: !blackBars + }); + + return ( + <> + + + {(cover || size.width > 80) && extension && ( +
+ {extension} +
+ )} + + ); + } + ) +); + +interface ThumbProps extends ThumbnailProps { + src?: string; + kind: ObjectKindKey; + path: string | null; + isDir: boolean; + frame: boolean; + fileId: number | null; + onLoad: () => void; + onError: (error: Error | ErrorEvent | SyntheticEvent) => void; + thumbType: ThumbType; + extension: string | null; + customIcon: IconTypes | null; + locationId: number | null; + pauseVideo: boolean; + magnification: number; + mediaControls: boolean; + frameClassName: string; + isSidebarPreview: boolean; +} + +const Thumb = memo( + forwardRef( + ( + { + src, + kind, + path, + frame, + isDir, + cover, + fileId, + thumbType, + extension, + blackBars, + className, + pauseVideo, + locationId, + customIcon, + magnification, + mediaControls, + blackBarsSize, + videoExtension, + frameClassName, + isSidebarPreview, + onLoad, + ...props + }, + _ref + ) => { + const ref = useRef(null); + useImperativeHandle( + _ref, + () => ref.current + ); + const [isLoading, setIsLoading] = useState(true); + + const handleLoad = useCallback(() => { + const img = ref.current; + setIsLoading(!(img == null || (img.naturalHeight > 0 && img.naturalWidth > 0))); + onLoad?.(); + }, [onLoad]); + + let thumb: JSX.Element | null = null; + + switch (thumbType) { + case 'original': + thumb = ( + + ); + break; + case 'thumbnail': + thumb = ( + + ); + break; + } + + return ( + <> + {}} + decoding="sync" + draggable={false} + extension={extension} + className={clsx(ThumbClasses, className, !isLoading && 'hidden')} + customIcon={customIcon} + /> + {thumb ?? null} + + ); + } + ) +); + +export interface FileThumbProps { data: ExplorerItem; loadOriginal?: boolean; size?: number; cover?: boolean; frame?: boolean; - onLoad?: (state: ThumbType) => void; - onError?: (state: ThumbType, error: Error) => void; + onLoad?: (type: ThumbType) => void; + onError?: (state: LoadState, error: Error) => void; blackBars?: boolean; blackBarsSize?: number; extension?: boolean; @@ -43,253 +259,181 @@ export interface ThumbProps { magnification?: number; } -type ThumbType = { variant: 'original' } | { variant: 'thumbnail' } | { variant: 'icon' }; -type LoadState = { - [K in 'original' | 'thumbnail' | 'icon']: 'notLoaded' | 'loaded' | 'error'; -}; +/** + * This component is used to render a thumbnail of a file or folder. + * It will automatically choose the best thumbnail to display based on the item data. + * + * .. WARNING:: + * This Component is heavely used inside the explorer, and as such it is a performance critical component. + * Be careful with the performance of the code, make sure to always memoize any objects or functions to avoid unnecessary re-renders. + * + */ +export const FileThumb = memo( + forwardRef((props, ref) => { + const frame = useFrame(); + const platform = usePlatform(); + const itemData = useExplorerItemData(props.data); + const filePath = getItemFilePath(props.data); + const { library } = useLibraryContext(); + const [loadState, setLoadState] = useState({ + icon: 'normal', + original: 'normal', + thumbnail: 'normal' + }); -export const FileThumb = forwardRef((props, ref) => { - const isDark = useIsDark(); - const platform = usePlatform(); - const frame = useFrame(); + // WARNING: This is required so QuickPreview can work properly + useEffect(() => { + setLoadState({ + icon: 'normal', + original: 'normal', + thumbnail: 'normal' + }); + }, [props.data]); - const itemData = useExplorerItemData(props.data); - const filePath = getItemFilePath(props.data); + const thumbType = useMemo((): ThumbType => { + if (loadState.original !== 'error' && props.loadOriginal) return 'original'; + if (loadState.thumbnail !== 'error' && itemData.hasLocalThumbnail) return 'thumbnail'; + return 'icon'; + }, [itemData.hasLocalThumbnail, loadState, props.loadOriginal]); - const { library } = useLibraryContext(); - - const [loadState, setLoadState] = useState({ - original: 'notLoaded', - thumbnail: 'notLoaded', - icon: 'notLoaded' - }); - - const childClassName = 'max-h-full max-w-full object-contain'; - const frameClassName = clsx(frame.className, props.frameClassName); - - const thumbType = useMemo(() => { - const thumbType = 'thumbnail'; - - if (thumbType === 'thumbnail') - if ( - loadState.thumbnail !== 'error' && - itemData.hasLocalThumbnail && - itemData.thumbnailKey - ) - return { variant: 'thumbnail' }; - - return { variant: 'icon' }; - }, [itemData, loadState]); - - const src = useMemo(() => { - switch (thumbType.variant) { - case 'original': - if (filePath && (itemData.extension !== 'pdf' || pdfViewerEnabled())) { - if ('id' in filePath && itemData.locationId) - return platform.getFileUrl(library.uuid, itemData.locationId, filePath.id); - else if ('path' in filePath) return platform.getFileUrlByPath(filePath.path); + useEffect(() => { + let timeoutId = null; + // Reload thumbnail when it gets a notification from core that it has been generated + if (thumbType === 'icon' && loadState.thumbnail === 'error') { + for (const [, thumbId] of itemData.thumbnails) { + if (thumbId == null || !explorerStore.newThumbnails.has(thumbId)) continue; + // HACK: Delay removing the new thumbnail event from store + // to avoid some weird race condition with core that prevents + // us from accessing the new thumbnail immediately after it is created + timeoutId = setTimeout(() => explorerStore.removeThumbnail(thumbId), 0); + explorerStore.removeThumbnail(thumbId); + setLoadState((state) => ({ ...state, thumbnail: 'normal' })); + break; } - break; + } - case 'thumbnail': - if (itemData.thumbnailKey) - return platform.getThumbnailUrlByThumbKey(itemData.thumbnailKey); + return () => void (timeoutId && clearTimeout(timeoutId)); + }, [itemData.thumbnails, loadState.thumbnail, thumbType]); - break; - case 'icon': - if (itemData.customIcon) return getIconByName(itemData.customIcon as any, isDark); + const src = useMemo(() => { + switch (thumbType) { + case 'original': + if (filePath && (itemData.extension !== 'pdf' || pdfViewerEnabled())) { + if ('id' in filePath && itemData.locationId) + return platform.getFileUrl( + library.uuid, + itemData.locationId, + filePath.id + ); + else if ('path' in filePath) + return platform.getFileUrlByPath(filePath.path); + else setLoadState((state) => ({ ...state, [thumbType]: 'error' })); + } + break; - return getIcon( - // itemData.isDir || parent?.type === 'Node' ? 'Folder' : - itemData.kind, - isDark, - itemData.extension, - itemData.isDir - ); - } - }, [filePath, isDark, library.uuid, itemData, platform, thumbType]); + case 'thumbnail': { + const thumbnail = Array.from(itemData.thumbnails.keys()).find((key) => key); + if (thumbnail) return thumbnail; + else setLoadState((state) => ({ ...state, [thumbType]: 'error' })); - const onLoad = (s: 'original' | 'thumbnail' | 'icon') => { - setLoadState((state) => ({ ...state, [s]: 'loaded' })); - props.onLoad?.call(null, thumbType); - }; + break; + } + } + }, [ + filePath, + itemData.extension, + itemData.locationId, + itemData.thumbnails, + library.uuid, + platform, + thumbType + ]); - const onError = ( - s: 'original' | 'thumbnail' | 'icon', - event: ErrorEvent | SyntheticEvent - ) => { - setLoadState((state) => ({ ...state, [s]: 'error' })); + const onError = useCallback( + (event: Error | ErrorEvent | SyntheticEvent) => { + const rawError = + event instanceof Error + ? event + : ('error' in event && event.error) || + ('message' in event && event.message) || + 'Filetype is not supported yet'; - const rawError = - ('error' in event && event.error) || - ('message' in event && event.message) || - 'Filetype is not supported yet'; - - props.onError?.call( - null, - thumbType, - rawError instanceof Error ? rawError : new Error(rawError) + setLoadState((state) => { + state = { ...state, [thumbType]: 'error' }; + props.onError?.call( + null, + state, + rawError instanceof Error ? rawError : new Error(rawError) + ); + return state; + }); + }, + [props.onError, thumbType] ); - }; - const _childClassName = - typeof props.childClassName === 'function' - ? props.childClassName(thumbType) - : props.childClassName; + const onLoad = useCallback(() => { + props.onLoad?.call(null, thumbType); + }, [props.onLoad, thumbType]); - const className = clsx(childClassName, _childClassName); - - const thumbnail = (() => { - if (!src) return <>; - - switch (thumbType.variant) { - case 'thumbnail': - return ( - onLoad('thumbnail')} - onError={(e) => onError('thumbnail', e)} - decoding={props.size ? 'async' : 'sync'} - className={clsx( - props.cover - ? [ - 'min-h-full min-w-full object-cover object-center', - _childClassName - ] - : className, - props.frame && !(itemData.kind === 'Video' && props.blackBars) - ? frameClassName - : null - )} - crossOrigin="anonymous" // Here it is ok, because it is not a react attr - blackBars={props.blackBars && itemData.kind === 'Video' && !props.cover} - blackBarsSize={props.blackBarsSize} - extension={ - props.extension && itemData.extension && itemData.kind === 'Video' - ? itemData.extension - : undefined - } - /> - ); - - case 'icon': - return ( - + { + console.error('ErrorBoundary', error, info); + onError(error); + }, + [onError] + )} + > + onLoad('icon')} - onError={(e) => onError('icon', e)} - decoding={props.size ? 'async' : 'sync'} - className={className} - draggable={false} - /> - ); - default: - return <>; - } - })(); - - return ( -
- {props.loadOriginal ? ( - - onLoad('original')} - onError={(e) => onError('original', e)} - filePath={filePath} - className={className} - frameClassName={frameClassName} - itemData={itemData} - isDark={isDark} - childClassName={childClassName} - size={props.size} - magnification={props.magnification} - mediaControls={props.mediaControls} - frame={props.frame} - isSidebarPreview={props.isSidebarPreview} - pauseVideo={props.pauseVideo} blackBars={props.blackBars} + className={ + typeof props.childClassName === 'function' + ? props.childClassName(thumbType) + : props.childClassName + } + customIcon={itemData.customIcon as IconTypes | null} + locationId={itemData.locationId} + pauseVideo={props.pauseVideo ?? false} blackBarsSize={props.blackBarsSize} + mediaControls={props.mediaControls ?? false} + magnification={props.magnification ?? 1} + frameClassName={clsx(frame.className, props.frameClassName)} + videoExtension={ + props.extension && itemData.extension && itemData.kind === 'Video' + ? itemData.extension + : undefined + } + isSidebarPreview={props.isSidebarPreview ?? false} /> - - ) : ( - thumbnail - )} -
- ); -}); - -interface ThumbnailProps extends Omit { - cover?: boolean; - blackBars?: boolean; - blackBarsSize?: number; - extension?: string; -} - -const Thumbnail = forwardRef( - ({ blackBars, blackBarsSize, extension, cover, className, style, ...props }, _ref) => { - const ref = useRef(null); - useImperativeHandle( - _ref, - () => ref.current +
+
); - - const size = useSize(ref); - - const { style: blackBarsStyle } = useBlackBars(ref, size, { - size: blackBarsSize, - disabled: !blackBars - }); - - return ( - <> - - - {(cover || size.width > 80) && extension && ( -
- {extension} -
- )} - - ); - } + }) ); diff --git a/interface/app/$libraryId/Explorer/Inspector/FavoriteButton.tsx b/interface/app/$libraryId/Explorer/Inspector/FavoriteButton.tsx index 97abc0d27..71eea2117 100644 --- a/interface/app/$libraryId/Explorer/Inspector/FavoriteButton.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/FavoriteButton.tsx @@ -14,7 +14,7 @@ export default function FavoriteButton(props: Props) { setFavorite(!!props.data?.favorite); }, [props.data]); - const { mutate: fileToggleFavorite, isLoading: isFavoriteLoading } = useLibraryMutation( + const { mutate: fileToggleFavorite, isPending: isFavoriteLoading } = useLibraryMutation( 'files.setFavorite' // { // onError: () => setFavorite(!!props.data?.favorite) diff --git a/interface/app/$libraryId/Explorer/Inspector/index.tsx b/interface/app/$libraryId/Explorer/Inspector/index.tsx index 08b3255d0..7d0226396 100644 --- a/interface/app/$libraryId/Explorer/Inspector/index.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/index.tsx @@ -154,7 +154,7 @@ const Thumbnails = ({ items }: { items: ExplorerItem[] }) => { i === 2 && 'z-10 !h-[84%] !w-[84%] rotate-[7deg]' )} childClassName={(type) => - type.variant !== 'icon' && thumbs.length > 1 + type !== 'icon' && thumbs.length > 1 ? 'shadow-md shadow-app-shade' : undefined } diff --git a/interface/app/$libraryId/Explorer/QuickPreview/index.tsx b/interface/app/$libraryId/Explorer/QuickPreview/index.tsx index ad1b0fdc5..918af62c9 100644 --- a/interface/app/$libraryId/Explorer/QuickPreview/index.tsx +++ b/interface/app/$libraryId/Explorer/QuickPreview/index.tsx @@ -50,7 +50,7 @@ import ExplorerContextMenu, { SharedItems } from '../ContextMenu'; import { Conditional } from '../ContextMenu/ConditionalItem'; -import { FileThumb } from '../FilePath/Thumb'; +import { FileThumb, ThumbType } from '../FilePath/Thumb'; import { SingleItemMetadata } from '../Inspector'; import { explorerStore } from '../store'; import { useExplorerViewContext } from '../View/Context'; @@ -84,19 +84,31 @@ export const QuickPreview = () => { const { open, itemIndex } = useQuickPreviewStore(); const thumb = createRef(); - const [thumbErrorToast, setThumbErrorToast] = useState(); const [showMetadata, setShowMetadata] = useState(false); const [magnification, setMagnification] = useState(1); const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); const [isRenaming, setIsRenaming] = useState(false); const [newName, setNewName] = useState(null); - const [thumbnailLoading, setThumbnailLoading] = useState<'notLoaded' | 'loaded' | 'error'>( - 'notLoaded' - ); + const [thumbnailLoading, setThumbnailLoading] = useState({ + icon: 'notLoaded', + thumbnail: 'notLoaded', + original: 'notLoaded' + } as { + [K in ThumbType]: 'notLoaded' | 'loaded' | 'error'; + }); // the purpose of these refs is to prevent "jittering" when zooming with trackpads, as the deltaY value can be very high const deltaYRef = useRef(0); const lastZoomTimeRef = useRef(0); + const hasError = useMemo( + () => Object.values(thumbnailLoading).some((status) => status === 'error'), + [thumbnailLoading] + ); + const isLoaded = useMemo( + () => Object.values(thumbnailLoading).some((status) => status === 'loaded'), + [thumbnailLoading] + ); + const { t } = useLocale(); const items = useMemo(() => { @@ -122,50 +134,26 @@ export const QuickPreview = () => { const renameFile = useLibraryMutation(['files.renameFile'], { onError: () => setNewName(null), - onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths']) + onSuccess: () => rspc.queryClient.invalidateQueries({ queryKey: ['search.paths'] }) }); const renameEphemeralFile = useLibraryMutation(['ephemeralFiles.renameFile'], { onError: () => setNewName(null), - onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths']) + onSuccess: () => rspc.queryClient.invalidateQueries({ queryKey: ['search.paths'] }) }); const changeCurrentItem = (index: number) => { if (items[index]) getQuickPreviewStore().itemIndex = index; }; - // Error toast - useEffect(() => { - if (!thumbErrorToast) return; - - let id: string | number | undefined; - toast.error( - (_id) => { - id = _id; - return thumbErrorToast; - }, - { - ref: thumb, - duration: Infinity, - onClose() { - id = undefined; - setThumbErrorToast(undefined); - } - } - ); - - return () => void toast.dismiss(id); - }, [thumb, thumbErrorToast]); - // Reset state useEffect(() => { setNewName(null); - setThumbErrorToast(undefined); setMagnification(1); + setThumbnailLoading({ icon: 'notLoaded', thumbnail: 'notLoaded', original: 'notLoaded' }); if (open || item) return; - setThumbnailLoading('notLoaded'); getQuickPreviewStore().open = false; getQuickPreviewStore().itemIndex = 0; setShowMetadata(false); @@ -344,18 +332,12 @@ export const QuickPreview = () => { )} >
- {thumbnailLoading !== 'error' && - thumbnailLoading !== 'notLoaded' && - background && ( -
- -
-
- )} + {!hasError && isLoaded && background && ( +
+ +
+
+ )}
{ - {thumbnailLoading === 'error' && ( - -
- -

- {t('quickpreview_thumbnail_error_message')} -

-
-
- )} + {thumbnailLoading.original === 'error' && + thumbnailLoading.thumbnail === 'loaded' && ( + +
+ +

+ {t( + 'quickpreview_thumbnail_error_message' + )} +

+
+
+ )} {items.length > 1 && (
@@ -616,56 +603,42 @@ export const QuickPreview = () => {
- {thumbnailLoading === 'error' ? ( - <> - - - ) : ( - { - setThumbnailLoading('loaded'); - if (type.variant === 'original') - setThumbErrorToast(undefined); - }} - onError={(type, error) => { - setThumbnailLoading('error'); - if (type.variant === 'original') - setThumbErrorToast({ - title: t('error_loading_original_file'), - body: error.message - }); - }} - loadOriginal - frameClassName="!border-0" - mediaControls - className={clsx( - thumbnailLoading === 'notLoaded' && 'hidden', - 'm-3 !w-auto flex-1 !overflow-hidden rounded', - !background && !icon && 'bg-app-box shadow' - )} - childClassName={clsx( - 'rounded', - kind === 'Text' && 'p-3', - !icon && 'h-full', - textKinds.includes(kind) && 'select-text' - )} - magnification={magnification} - /> - )} + { + setThumbnailLoading((obj) => ({ + ...obj, + [type]: 'loaded' + })); + }} + onError={(state, error) => { + console.error(error); + setThumbnailLoading((obj) => { + const newState = { ...obj }; + for (const [type, loadState] of Object.entries( + state + ) as [ThumbType, string][]) + if (loadState === 'error') newState[type] = 'error'; + + return newState; + }); + }} + loadOriginal + frameClassName="!border-0" + mediaControls + className={clsx( + !isLoaded && 'hidden', + 'm-3 !w-auto flex-1 !overflow-hidden rounded', + !background && !icon && 'bg-app-box shadow' + )} + childClassName={clsx( + 'rounded', + kind === 'Text' && 'p-3', + !icon && 'h-full', + textKinds.includes(kind) && 'select-text' + )} + magnification={magnification} + /> {explorerLayoutStore.showImageSlider && activeItem && ( diff --git a/interface/app/$libraryId/Explorer/View/Grid/DragSelect/index.tsx b/interface/app/$libraryId/Explorer/View/Grid/DragSelect/index.tsx index e58a56787..e93f85355 100644 --- a/interface/app/$libraryId/Explorer/View/Grid/DragSelect/index.tsx +++ b/interface/app/$libraryId/Explorer/View/Grid/DragSelect/index.tsx @@ -13,8 +13,16 @@ import { getElementIndex, SELECTABLE_DATA_ATTRIBUTE } from './util'; const CHROME_REGEX = /Chrome/; +type GridOpts = ReturnType>; + interface Props extends PropsWithChildren { - grid: ReturnType>; + columnCount: GridOpts['columnCount']; + gapY: GridOpts['gap']['y']; + getItem: GridOpts['getItem']; + totalColumnCount: GridOpts['totalColumnCount']; + totalCount: GridOpts['totalCount']; + totalRowCount: GridOpts['totalRowCount']; + virtualItemHeight: GridOpts['virtualItemHeight']; } export interface Drag { @@ -24,7 +32,7 @@ export interface Drag { endRow: number; } -export const DragSelect = ({ grid, children }: Props) => { +export const DragSelect = ({ children, ...props }: Props) => { const isChrome = CHROME_REGEX.test(navigator.userAgent); const { explorerOperatingSystem, matchingOperatingSystem } = useExplorerOperatingSystem(); @@ -62,7 +70,7 @@ export const DragSelect = ({ grid, children }: Props) => { function getGridItem(element: Element) { const index = getElementIndex(element); - return (index !== null && grid.getItem(index)) || undefined; + return (index !== null && props.getItem(index)) || undefined; } function handleScroll(e: SelectoEvents['scroll']) { @@ -176,9 +184,9 @@ export const DragSelect = ({ grid, children }: Props) => { // that are still in the DOM const elements: Element[] = []; - e.added.forEach((element) => { + for (const element of e.added) { const item = getGridItem(element); - if (!item?.data) return; + if (!item?.data) continue; // Add item to selected targets // Don't update selecto as it's already aware of it @@ -188,22 +196,22 @@ export const DragSelect = ({ grid, children }: Props) => { explorer.addSelectedItem(item.data); if (document.contains(element)) elements.push(element); - }); + } - e.removed.forEach((element) => { + for (const element of e.removed) { const item = getGridItem(element); - if (!item?.data) return; + if (!item?.data) continue; // Remove item from selected targets // Don't update selecto as it's already aware of it selectedTargets.removeSelectedTarget(String(item.id), { updateSelecto: false }); // Don't deselect item if element is unmounted by scroll - if (!document.contains(element)) return; + if (!document.contains(element)) continue; explorer.removeSelectedItem(item.data); elements.push(element); - }); + } const dragDirection = { x: inputEvent.x === e.rect.left ? 'left' : 'right', @@ -280,7 +288,7 @@ export const DragSelect = ({ grid, children }: Props) => { const addedRows = new Set(); const removedRows = new Set(); - columns.forEach((column) => { + for (const column of columns) { const { firstItem, lastItem } = columnItems[column]!; const { row: firstRow } = firstItem.item; @@ -353,7 +361,7 @@ export const DragSelect = ({ grid, children }: Props) => { // Remove row if dragged out of the last grid item // from a row that's above it - if (item.item.index === grid.totalCount - 1) { + if (item.item.index === props.totalCount - 1) { removedRows.add(item.item.row); } } @@ -372,21 +380,21 @@ export const DragSelect = ({ grid, children }: Props) => { // caches multiple rows at once, and the first one being removed if ( !isFirstRowInDrag && - firstRow === grid.totalRowCount - 2 && - firstItem.item.index + grid.totalColumnCount > grid.totalCount - 1 + firstRow === props.totalRowCount - 2 && + firstItem.item.index + props.totalColumnCount > props.totalCount - 1 ) { removedColumns.add(column); } // Return if first row equals the first/last row of the grid (depending on drag direction) // as there's no items to be selected beyond that point - if (!drag.current && (firstRow === 0 || firstRow === grid.totalRowCount - 1)) { - return; + if (!drag.current && (firstRow === 0 || firstRow === props.totalRowCount - 1)) { + continue; } // Return if column is already in drag range if (isColumnInDrag && isColumnInDragRange) { - return; + continue; } const viewTop = explorerView.ref.current?.getBoundingClientRect().top ?? 0; @@ -397,9 +405,9 @@ export const DragSelect = ({ grid, children }: Props) => { const hasEmptySpace = dragDirection.y === 'down' ? dragStart.y < itemTop : dragStart.y > itemBottom; - if (!hasEmptySpace) return; + if (!hasEmptySpace) continue; - // Get the heigh of the empty drag space between the start of the drag + // Get the height of the empty drag space between the start of the drag // and the first visible item const emptySpaceHeight = Math.abs( dragStart.y - (dragDirection.y === 'down' ? itemTop : itemBottom) @@ -407,8 +415,8 @@ export const DragSelect = ({ grid, children }: Props) => { // Check how many items we can fit into the empty space let itemsInEmptySpace = - (emptySpaceHeight - (grid.gap.y ?? 0)) / - (grid.virtualItemHeight + (grid.gap.y ?? 0)); + (emptySpaceHeight - (props.gapY ?? 0)) / + (props.virtualItemHeight + (props.gapY ?? 0)); if (itemsInEmptySpace > 1) { itemsInEmptySpace = Math.ceil(itemsInEmptySpace); @@ -416,15 +424,15 @@ export const DragSelect = ({ grid, children }: Props) => { itemsInEmptySpace = Math.round(itemsInEmptySpace); } - [...Array(itemsInEmptySpace)].forEach((_, i) => { + for (let i = 0; i < itemsInEmptySpace; i++) { i = dragDirection.y === 'down' ? itemsInEmptySpace - i : i + 1; const explorerItemIndex = firstItem.item.index + - (dragDirection.y === 'down' ? -i : i) * grid.columnCount; + (dragDirection.y === 'down' ? -i : i) * props.columnCount; - const item = grid.getItem(explorerItemIndex); - if (!item?.data) return; + const item = props.getItem(explorerItemIndex); + if (!item?.data) continue; // Set start row if not already set if (!drag.current && i === itemsInEmptySpace - 1) { @@ -438,13 +446,13 @@ export const DragSelect = ({ grid, children }: Props) => { explorer.addSelectedItem(item.data); } - return; + continue; } if (!isItemInDrag) explorer.removeSelectedItem(item.data); else explorer.addSelectedItem(item.data); - }); - }); + } + } const addedColumnsArray = [...addedColumns]; const removedColumnsArray = [...removedColumns]; diff --git a/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx b/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx index aeb40ee0f..a9d01994f 100644 --- a/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx +++ b/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx @@ -52,9 +52,10 @@ export const GridViewItem = memo((props: GridViewItemProps) => { ); }); -const InnerDroppable = () => { +const InnerDroppable = memo(() => { const item = useGridViewItemContext(); const { isDroppable } = useExplorerDroppableContext(); + return ( <>
{ (item.selected || isDroppable) && 'bg-app-selectedItem' )} > - +
); -}; +}); -const ItemFileThumb = () => { +const ItemFileThumb = memo((props: GridViewItemProps) => { const frame = useFrame(); - - const item = useGridViewItemContext(); - const isLabel = item.data.type === 'Label'; - const { attributes, listeners, style, setDraggableRef } = useExplorerDraggable({ - data: item.data + data: props.data }); + const isLabel = props.data.type === 'Label'; + return ( ({ + style, + ...attributes, + ...listeners + }), + [style, attributes, listeners] + )} /> ); -}; +}); -const ItemMetadata = () => { +const ItemMetadata = memo(() => { const item = useGridViewItemContext(); const { isDroppable } = useExplorerDroppableContext(); const explorerLayout = useExplorerLayoutStore(); @@ -123,9 +125,9 @@ const ItemMetadata = () => { {item.data.type === 'Label' && } ); -}; +}); -const ItemTags = () => { +const ItemTags = memo(() => { const item = useGridViewItemContext(); const object = getItemObject(item.data); const filePath = getItemFilePath(item.data); @@ -150,9 +152,9 @@ const ItemTags = () => { ))}
); -}; +}); -const ItemSize = () => { +const ItemSize = memo(() => { const item = useGridViewItemContext(); const { showBytesInGridView } = useExplorerContext().useSettingsSnapshot(); const isRenaming = useSelector(explorerStore, (s) => s.isRenaming); @@ -186,9 +188,9 @@ const ItemSize = () => { {`${bytes}`}
); -}; +}); -function LabelItemCount({ data }: { data: Extract }) { +const LabelItemCount = memo(({ data }: { data: Extract }) => { const { t } = useLocale(); const count = useLibraryQuery([ @@ -202,11 +204,11 @@ function LabelItemCount({ data }: { data: Extract {t('item_with_count', { count: count.data })}
); -} +}); diff --git a/interface/app/$libraryId/Explorer/View/GridView/index.tsx b/interface/app/$libraryId/Explorer/View/GridView/index.tsx index b5055ee49..507e5ee7d 100644 --- a/interface/app/$libraryId/Explorer/View/GridView/index.tsx +++ b/interface/app/$libraryId/Explorer/View/GridView/index.tsx @@ -1,5 +1,5 @@ import { Grid, useGrid } from '@virtual-grid/react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback } from 'react'; import { useExplorerLayoutStore } from '@sd/client'; import { useExplorerContext } from '../../Context'; @@ -50,7 +50,15 @@ export const GridView = () => { useKeySelection(grid, { scrollToEnd: true }); return ( - + {(index) => { const item = explorer.items?.[index]; diff --git a/interface/app/$libraryId/Explorer/View/ListView/index.tsx b/interface/app/$libraryId/Explorer/View/ListView/index.tsx index fc6ac953d..e792a19ca 100644 --- a/interface/app/$libraryId/Explorer/View/ListView/index.tsx +++ b/interface/app/$libraryId/Explorer/View/ListView/index.tsx @@ -130,47 +130,49 @@ export const ListView = memo(() => { const [backRange, frontRange] = getRangesByRow(range.start); if (backRange && frontRange) { - [...Array(backRange.sorted.end.index - backRange.sorted.start.index + 1)].forEach( - (_, i) => { - const index = backRange.sorted.start.index + i; + for (let i = backRange.sorted.start.index; i <= backRange.sorted.end.index; i++) { + const index = backRange.sorted.start.index + i; - if (index === range.start.index) return; + if (index === range.start.index) continue; - const row = rows[index]; + const row = rows[index]; - if (row) explorer.removeSelectedItem(row.original); - } - ); + if (row) explorer.removeSelectedItem(row.original); + } _ranges = _ranges.filter((_, i) => i !== backRange.index); } - [...Array(Math.abs(range.end.index - rowIndex) + (changeDirection ? 1 : 0))].forEach( - (_, i) => { - if (!range.direction || direction === range.direction) i += 1; + for ( + let i = 0; + i < Math.abs(range.end.index - rowIndex) + (changeDirection ? 1 : 0); + i++ + ) { + if (!range.direction || direction === range.direction) i += 1; - const index = range.end.index + (direction === 'down' ? i : -i); + const index = range.end.index + (direction === 'down' ? i : -i); - const row = rows[index]; + const row = rows[index]; - if (!row) return; + if (!row) continue; - const item = row.original; + const item = row.original; - if (uniqueId(item) === uniqueId(range.start.original)) return; + if (uniqueId(item) === uniqueId(range.start.original)) continue; - if ( - !range.direction || - direction === range.direction || - (changeDirection && - (range.direction === 'down' - ? index < range.start.index - : index > range.start.index)) - ) { - explorer.addSelectedItem(item); - } else explorer.removeSelectedItem(item); + if ( + !range.direction || + direction === range.direction || + (changeDirection && + (range.direction === 'down' + ? index < range.start.index + : index > range.start.index)) + ) { + explorer.addSelectedItem(item); + } else { + explorer.removeSelectedItem(item); } - ); + } let newRangeEnd = item; let removeRangeIndex: number | null = null; @@ -186,15 +188,13 @@ export const ListView = memo(() => { rowIndex ); - [...Array(removableRowsCount)].forEach((_, i) => { - i += 1; - + for (let i = 1; i <= removableRowsCount; i++) { const index = rowIndex + (direction === 'down' ? i : -i); const row = rows[index]; if (row) explorer.removeSelectedItem(row.original); - }); + } removeRangeIndex = i; break; diff --git a/interface/app/$libraryId/Explorer/View/MediaView/Item.tsx b/interface/app/$libraryId/Explorer/View/MediaView/Item.tsx index c5b1652cf..dd71f2b31 100644 --- a/interface/app/$libraryId/Explorer/View/MediaView/Item.tsx +++ b/interface/app/$libraryId/Explorer/View/MediaView/Item.tsx @@ -57,7 +57,7 @@ const ItemFileThumb = (props: Pick) => { filePath?.hidden && 'opacity-50' )} ref={setDraggableRef} - childClassName={({ variant }) => clsx(variant === 'icon' && 'size-2/4')} + childClassName={(type) => clsx(type === 'icon' && 'size-2/4')} childProps={{ style, ...attributes, diff --git a/interface/app/$libraryId/Explorer/View/MediaView/index.tsx b/interface/app/$libraryId/Explorer/View/MediaView/index.tsx index 6554d6a7d..cf8f6f866 100644 --- a/interface/app/$libraryId/Explorer/View/MediaView/index.tsx +++ b/interface/app/$libraryId/Explorer/View/MediaView/index.tsx @@ -82,11 +82,12 @@ export const MediaView = () => { let firstRowIndex: number | undefined = undefined; let lastRowIndex: number | undefined = undefined; + const scrollOffset = rowVirtualizer.scrollOffset ?? 0; // Find first row in viewport for (let i = 0; i < virtualRows.length; i++) { const row = virtualRows[i]!; - if (row.end >= rowVirtualizer.scrollOffset) { + if (row.end >= scrollOffset) { firstRowIndex = row.index; break; } @@ -95,7 +96,7 @@ export const MediaView = () => { // Find last row in viewport for (let i = virtualRows.length - 1; i >= 0; i--) { const row = virtualRows[i]!; - if (row.start <= rowVirtualizer.scrollOffset + rowVirtualizer.scrollRect.height) { + if (row.start <= scrollOffset + (rowVirtualizer.scrollRect?.height ?? 0)) { lastRowIndex = row.index; break; } @@ -163,15 +164,16 @@ export const MediaView = () => { ); } }, [ + isSortingByDate, + orderBy, + orderDirection, explorer.items, + rowVirtualizer.scrollOffset, + rowVirtualizer.scrollRect?.height, grid.columnCount, grid.options.count, - isSortingByDate, - rowVirtualizer.scrollOffset, - rowVirtualizer.scrollRect.height, - virtualRows, - orderBy, - orderDirection + dateFormat, + virtualRows ]); useKeySelection(grid); @@ -187,7 +189,15 @@ export const MediaView = () => { > {isSortingByDate && } - + {virtualRows.map((virtualRow) => ( {columnVirtualizer.getVirtualItems().map((virtualColumn) => { diff --git a/interface/app/$libraryId/Explorer/View/RenamableItemText.tsx b/interface/app/$libraryId/Explorer/View/RenamableItemText.tsx index b3852082e..2b53e43c8 100644 --- a/interface/app/$libraryId/Explorer/View/RenamableItemText.tsx +++ b/interface/app/$libraryId/Explorer/View/RenamableItemText.tsx @@ -58,17 +58,17 @@ export const RenamableItemText = ({ const renameFile = useLibraryMutation(['files.renameFile'], { onError: () => reset(), - onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths']) + onSuccess: () => rspc.queryClient.invalidateQueries({ queryKey: ['search.paths'] }) }); const renameEphemeralFile = useLibraryMutation(['ephemeralFiles.renameFile'], { onError: () => reset(), - onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths']) + onSuccess: () => rspc.queryClient.invalidateQueries({ queryKey: ['search.paths'] }) }); const renameLocation = useLibraryMutation(['locations.update'], { onError: () => reset(), - onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths']) + onSuccess: () => rspc.queryClient.invalidateQueries({ queryKey: ['search.paths'] }) }); const reset = useCallback(() => { diff --git a/interface/app/$libraryId/Explorer/View/ViewItem.tsx b/interface/app/$libraryId/Explorer/View/ViewItem.tsx index 2537b854f..087c8b808 100644 --- a/interface/app/$libraryId/Explorer/View/ViewItem.tsx +++ b/interface/app/$libraryId/Explorer/View/ViewItem.tsx @@ -192,14 +192,15 @@ export const useViewItemDoubleClick = () => { } }, [ - searchParams, explorer.selectedItems, explorer.settingsStore.openOnDoubleClick, - library.uuid, - navigate, openFilePaths, - openEphemeralFiles, - updateAccessTime + updateAccessTime, + library.uuid, + t, + searchParams, + navigate, + openEphemeralFiles ] ); diff --git a/interface/app/$libraryId/Explorer/index.tsx b/interface/app/$libraryId/Explorer/index.tsx index 3dcb19261..2aa5971c0 100644 --- a/interface/app/$libraryId/Explorer/index.tsx +++ b/interface/app/$libraryId/Explorer/index.tsx @@ -61,7 +61,7 @@ export default function Explorer(props: PropsWithChildren) { // I had planned to somehow fetch the Object, but its a lot more work than its worth given // id have to fetch the file_path explicitly and patch the query // for now, it seems to work a treat just invalidating the whole query - rspc.queryClient.invalidateQueries(['search.paths']); + rspc.queryClient.invalidateQueries({ queryKey: ['search.paths'] }); } } }); diff --git a/interface/app/$libraryId/Explorer/store.ts b/interface/app/$libraryId/Explorer/store.ts index 4814f972a..bb463ab11 100644 --- a/interface/app/$libraryId/Explorer/store.ts +++ b/interface/app/$libraryId/Explorer/store.ts @@ -123,8 +123,19 @@ export function flattenThumbnailKey(thumbKey: ThumbKey) { export const explorerStore = proxy({ ...state, reset: (_state?: typeof state) => resetStore(explorerStore, _state || state), - addNewThumbnail: (thumbKey: ThumbKey) => { - explorerStore.newThumbnails.add(flattenThumbnailKey(thumbKey)); + addNewThumbnail: (thumbKey: ThumbKey | string) => { + thumbKey = typeof thumbKey === 'string' ? thumbKey : flattenThumbnailKey(thumbKey); + // HACK: Ensure store propagates changes + const newThumbnails = new Set(explorerStore.newThumbnails); + newThumbnails.add(thumbKey); + explorerStore.newThumbnails = newThumbnails; + }, + removeThumbnail: (thumbKey: ThumbKey | string) => { + thumbKey = typeof thumbKey === 'string' ? thumbKey : flattenThumbnailKey(thumbKey); + // HACK: Ensure store propagates changes + const newThumbnails = new Set(explorerStore.newThumbnails); + newThumbnails.delete(thumbKey); + explorerStore.newThumbnails = newThumbnails; }, resetCache: () => { explorerStore.newThumbnails.clear(); diff --git a/interface/app/$libraryId/Explorer/useExplorer.ts b/interface/app/$libraryId/Explorer/useExplorer.ts index 97af144b8..c656362c7 100644 --- a/interface/app/$libraryId/Explorer/useExplorer.ts +++ b/interface/app/$libraryId/Explorer/useExplorer.ts @@ -105,7 +105,7 @@ export function useExplorerSettings({ >; data?: T | null; }) { - const [store] = useState(() => proxy(settings)); + const store = useMemo(() => proxy(settings), [settings]); const updateSettings = useDebouncedCallback((settings: ExplorerSettings, data: T) => { onSettingsChanged?.(settings, data); @@ -149,15 +149,8 @@ function useSelectedItems(items: ExplorerItem[] | null) { const itemHashesWeakMap = useRef(new WeakMap()); // Store hashes of items instead as objects are unique by reference but we - // still need to differentate between item variants - const [selectedItemHashes, setSelectedItemHashes] = useState(() => ({ - value: new Set() - })); - - const updateHashes = useCallback( - () => setSelectedItemHashes((h) => ({ ...h })), - [setSelectedItemHashes] - ); + // still need to differentiate between item variants + const [selectedItemHashes, setSelectedItemHashes] = useState(() => new Set()); const itemsMap = useMemo( () => @@ -172,7 +165,7 @@ function useSelectedItems(items: ExplorerItem[] | null) { const selectedItems = useMemo( () => - [...selectedItemHashes.value].reduce((items, hash) => { + [...selectedItemHashes].reduce((items, hash) => { const item = itemsMap.get(hash); if (item) items.add(item.data); return items; @@ -194,37 +187,37 @@ function useSelectedItems(items: ExplorerItem[] | null) { (item: ExplorerItem | ExplorerItem[]) => { const items = Array.isArray(item) ? item : [item]; - for (let i = 0; i < items.length; i++) { - selectedItemHashes.value.add(getItemUniqueId(items[i]!)); - } - - updateHashes(); + setSelectedItemHashes((oldHashes) => { + const newHashes = new Set(oldHashes); + for (const it of items) newHashes.add(getItemUniqueId(it)); + return newHashes; + }); }, - [getItemUniqueId, selectedItemHashes.value, updateHashes] + [getItemUniqueId] ), removeSelectedItem: useCallback( (item: ExplorerItem | ExplorerItem[]) => { const items = Array.isArray(item) ? item : [item]; - - for (let i = 0; i < items.length; i++) { - selectedItemHashes.value.delete(getItemUniqueId(items[i]!)); - } - - updateHashes(); + setSelectedItemHashes((oldHashes) => { + const newHashes = new Set(oldHashes); + for (const it of items) newHashes.delete(getItemUniqueId(it)); + return newHashes; + }); }, - [getItemUniqueId, selectedItemHashes.value, updateHashes] + [getItemUniqueId] ), resetSelectedItems: useCallback( (items?: ExplorerItem[]) => { - selectedItemHashes.value.clear(); - items?.forEach((item) => selectedItemHashes.value.add(getItemUniqueId(item))); - updateHashes(); + if (items) { + const newHashes = new Set(); + for (const it of items) newHashes.add(getItemUniqueId(it)); + setSelectedItemHashes(newHashes); + } else { + setSelectedItemHashes(new Set()); + } }, - [getItemUniqueId, selectedItemHashes.value, updateHashes] + [getItemUniqueId] ), - isItemSelected: useCallback( - (item: ExplorerItem) => selectedItems.has(item), - [selectedItems] - ) + isItemSelected: (item: ExplorerItem) => selectedItems.has(item) }; } diff --git a/interface/app/$libraryId/Explorer/useExplorerDraggable.tsx b/interface/app/$libraryId/Explorer/useExplorerDraggable.tsx index 39c23511f..b32105624 100644 --- a/interface/app/$libraryId/Explorer/useExplorerDraggable.tsx +++ b/interface/app/$libraryId/Explorer/useExplorerDraggable.tsx @@ -1,5 +1,5 @@ import { useDraggable, UseDraggableArguments } from '@dnd-kit/core'; -import { CSSProperties, HTMLAttributes } from 'react'; +import { CSSProperties, HTMLAttributes, useCallback, useMemo } from 'react'; import { ExplorerItem } from '@sd/client'; import { explorerStore } from './store'; @@ -11,8 +11,26 @@ export interface UseExplorerDraggableProps extends Omit { - const disabled = props.disabled || !draggableTypes.includes(props.data.type); + const disabled = useMemo( + () => props.disabled || !draggableTypes.includes(props.data.type), + [props.disabled, props.data.type] + ); const { setNodeRef, ...draggable } = useDraggable({ ...props, @@ -20,30 +38,30 @@ export const useExplorerDraggable = (props: UseExplorerDraggableProps) => { disabled: disabled }); - const onMouseDown = () => { + const onMouseDown = useCallback(() => { if (!disabled) explorerStore.drag = { type: 'touched' }; - }; + }, [disabled]); - const onMouseLeave = () => { + const onMouseLeave = useCallback(() => { if (explorerStore.drag?.type !== 'dragging') explorerStore.drag = null; - }; + }, []); - const onMouseUp = () => (explorerStore.drag = null); - - const style = { - cursor: 'default', - outline: 'none' - } satisfies CSSProperties; + const onMouseUp = useCallback(() => { + explorerStore.drag = null; + }, []); return { ...draggable, setDraggableRef: setNodeRef, - listeners: { - ...draggable.listeners, - onMouseDown, - onMouseLeave, - onMouseUp - } satisfies HTMLAttributes, - style + listeners: useMemo( + () => ({ + ...draggable.listeners, + onMouseDown, + onMouseLeave, + onMouseUp + }), + [draggable.listeners, onMouseDown, onMouseLeave, onMouseUp] + ) satisfies HTMLAttributes, + style: DRAGGABLE_STYLE }; }; diff --git a/interface/app/$libraryId/Explorer/useExplorerItemData.tsx b/interface/app/$libraryId/Explorer/useExplorerItemData.tsx index 06d2f2af9..3aff87488 100644 --- a/interface/app/$libraryId/Explorer/useExplorerItemData.tsx +++ b/interface/app/$libraryId/Explorer/useExplorerItemData.tsx @@ -1,34 +1,91 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { useEffect, useMemo } from 'react'; -import { getExplorerItemData, useSelector, type ExplorerItem } from '@sd/client'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { subscribe } from 'valtio'; +import { + compareHumanizedSizes, + getExplorerItemData, + humanizeSize, + ThumbKey, + type ExplorerItem +} from '@sd/client'; +import { usePlatform } from '~/util/Platform'; import { explorerStore, flattenThumbnailKey } from './store'; -// This is where we intercept the state of the explorer item to determine if we should rerender -// This hook is used inside every thumbnail in the explorer +/** + * This is where we intercept the state of the explorer item to determine if we should rerender + * + * .. WARNING:: + * This hook is used inside every thumbnail in the explorer. + * Be careful with the performance of the code, make sure to always memoize any objects or functions to avoid unnecessary re-renders. + * + * @param explorerItem - The explorer item to get data from + * @returns The extracted data from the explorer item + */ export function useExplorerItemData(explorerItem: ExplorerItem) { - const newThumbnail = useSelector(explorerStore, (s) => { - const thumbnailKey = - explorerItem.type === 'Label' - ? // labels have .thumbnails, plural - explorerItem.thumbnails?.[0] - : // all other explorer items have .thumbnail singular - 'thumbnail' in explorerItem && explorerItem.thumbnail; + const platform = usePlatform(); + const cachedSize = useRef | null>(null); + const [newThumbnails, setNewThumbnails] = useState>(new Map()); - return !!(thumbnailKey && s.newThumbnails.has(flattenThumbnailKey(thumbnailKey))); - }); + let thumbnails: ThumbKey | ThumbKey[] | null = null; + switch (explorerItem.type) { + case 'Label': + thumbnails = explorerItem.thumbnails; + break; + case 'Path': + case 'Object': + case 'NonIndexedPath': + thumbnails = explorerItem.thumbnail; + break; + } + + useEffect(() => { + const thumbnailKeys = thumbnails + ? Array.isArray(thumbnails) + ? thumbnails + : [thumbnails] + : []; + + const updateThumbnails = () => + setNewThumbnails((oldThumbs) => { + const thumbs = thumbnailKeys.reduce>((acc, thumbKey) => { + const url = platform.getThumbnailUrlByThumbKey(thumbKey); + const thumbId = flattenThumbnailKey(thumbKey); + acc.set(url, explorerStore.newThumbnails.has(thumbId) ? thumbId : null); + return acc; + }, new Map()); + + // Avoid unnecessary re-renders + return oldThumbs.size !== thumbs.size || + Array.from(oldThumbs.keys()).some( + (key) => oldThumbs.get(key) !== thumbs.get(key) + ) + ? thumbs + : oldThumbs; + }); + + updateThumbnails(); + + return subscribe(explorerStore, updateThumbnails); + }, [thumbnails, platform]); return useMemo(() => { - const itemData = getExplorerItemData(explorerItem); + const explorerItemData = getExplorerItemData(explorerItem); - if (!itemData.hasLocalThumbnail) { - itemData.hasLocalThumbnail = newThumbnail; + // Avoid unecessary re-renders + if ( + cachedSize.current == null || + !compareHumanizedSizes(cachedSize.current, explorerItemData.size) + ) { + cachedSize.current = explorerItemData.size; } - return itemData; - // whatever goes here, is what can cause an atomic re-render of an explorer item - // this is used for when new thumbnails are generated, and files identified - }, [explorerItem, newThumbnail]); + return { + ...explorerItemData, + size: cachedSize.current, + thumbnails: newThumbnails, + hasLocalThumbnail: explorerItemData.hasLocalThumbnail || newThumbnails.size > 0 + }; + }, [explorerItem, newThumbnails]); } export type ExplorerItemData = ReturnType; diff --git a/interface/app/$libraryId/Explorer/useExplorerPreferences.ts b/interface/app/$libraryId/Explorer/useExplorerPreferences.ts index 6af94ec7b..60d4bc3d5 100644 --- a/interface/app/$libraryId/Explorer/useExplorerPreferences.ts +++ b/interface/app/$libraryId/Explorer/useExplorerPreferences.ts @@ -53,7 +53,7 @@ export function useExplorerPreferences({ try { await updatePreferences.mutateAsync(writeSettings(settings)); - rspc.queryClient.invalidateQueries(['preferences.get']); + rspc.queryClient.invalidateQueries({ queryKey: ['preferences.get'] }); } catch (e) { alert('An error has occurred while updating your preferences.'); } diff --git a/interface/app/$libraryId/Layout/CMDK/index.tsx b/interface/app/$libraryId/Layout/CMDK/index.tsx index 1055e3708..7cd205911 100644 --- a/interface/app/$libraryId/Layout/CMDK/index.tsx +++ b/interface/app/$libraryId/Layout/CMDK/index.tsx @@ -1,6 +1,7 @@ import './CMDK.css'; import './CMDK.scss'; +import { keepPreviousData } from '@tanstack/react-query'; import clsx from 'clsx'; import { useEffect, useState } from 'react'; import CommandPalette, { filterItems, getItemIndex } from 'react-cmdk'; @@ -50,7 +51,9 @@ const CMDK = () => { const [page, setPage] = useState<'root' | 'locations' | 'tags'>('root'); const [search, setSearch] = useState(''); - const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true }); + const locationsQuery = useLibraryQuery(['locations.list'], { + placeholderData: keepPreviousData + }); const locations = locationsQuery.data; const onlineLocations = useOnlineLocations(); diff --git a/interface/app/$libraryId/Layout/CMDK/pages/CMDKLocations.tsx b/interface/app/$libraryId/Layout/CMDK/pages/CMDKLocations.tsx index f878dea0a..b0696c50b 100644 --- a/interface/app/$libraryId/Layout/CMDK/pages/CMDKLocations.tsx +++ b/interface/app/$libraryId/Layout/CMDK/pages/CMDKLocations.tsx @@ -1,3 +1,4 @@ +import { keepPreviousData } from '@tanstack/react-query'; import clsx from 'clsx'; import CommandPalette from 'react-cmdk'; import { useNavigate } from 'react-router'; @@ -5,7 +6,9 @@ import { arraysEqual, useLibraryQuery, useOnlineLocations } from '@sd/client'; import { Icon } from '~/components'; export default function CMDKLocations() { - const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true }); + const locationsQuery = useLibraryQuery(['locations.list'], { + placeholderData: keepPreviousData + }); const locations = locationsQuery.data; const onlineLocations = useOnlineLocations(); diff --git a/interface/app/$libraryId/Layout/CMDK/pages/CMDKTags.tsx b/interface/app/$libraryId/Layout/CMDK/pages/CMDKTags.tsx index a3cc2c836..7ca898691 100644 --- a/interface/app/$libraryId/Layout/CMDK/pages/CMDKTags.tsx +++ b/interface/app/$libraryId/Layout/CMDK/pages/CMDKTags.tsx @@ -1,9 +1,10 @@ +import { keepPreviousData } from '@tanstack/react-query'; import CommandPalette from 'react-cmdk'; import { useNavigate } from 'react-router'; import { useLibraryQuery, type Tag } from '@sd/client'; export default function CMDKTags() { - const result = useLibraryQuery(['tags.list'], { keepPreviousData: true }); + const result = useLibraryQuery(['tags.list'], { placeholderData: keepPreviousData }); const tags = result.data || []; const navigate = useNavigate(); diff --git a/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx b/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx index 21eec1971..5cb5d97e5 100644 --- a/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx @@ -154,15 +154,12 @@ export default () => { title="React Query Devtools" description="Configure the React Query devtools." > - + + (debugState.reactQueryDevtools = !debugState.reactQueryDevtools) + } + /> { - queryClient.invalidateQueries(['jobs.reports']); + queryClient.invalidateQueries({ queryKey: ['jobs.reports'] }); }) ); diff --git a/interface/app/$libraryId/Layout/Sidebar/JobManager/index.tsx b/interface/app/$libraryId/Layout/Sidebar/JobManager/index.tsx index 837dd0228..d317f4a31 100644 --- a/interface/app/$libraryId/Layout/Sidebar/JobManager/index.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/JobManager/index.tsx @@ -97,7 +97,7 @@ export function JobManager() { title: t('success'), body: t('all_jobs_have_been_cleared') }); - queryClient.invalidateQueries(['jobs.reports']); + queryClient.invalidateQueries({ queryKey: ['jobs.reports'] }); } catch (error) { toast.error({ title: t('error'), diff --git a/interface/app/$libraryId/Layout/Sidebar/SidebarLayout/Footer.tsx b/interface/app/$libraryId/Layout/Sidebar/SidebarLayout/Footer.tsx index 2d7dc4835..b8eb0bffc 100644 --- a/interface/app/$libraryId/Layout/Sidebar/SidebarLayout/Footer.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/SidebarLayout/Footer.tsx @@ -1,5 +1,5 @@ -import { inferSubscriptionResult } from '@oscartbeaumont-sd/rspc-client'; import { Gear } from '@phosphor-icons/react'; +import { inferSubscriptionResult } from '@spacedrive/rspc-client'; import { useState } from 'react'; import { useNavigate } from 'react-router'; import { diff --git a/interface/app/$libraryId/Layout/Sidebar/sections/Locations/index.tsx b/interface/app/$libraryId/Layout/Sidebar/sections/Locations/index.tsx index ef80dc378..703eddae0 100644 --- a/interface/app/$libraryId/Layout/Sidebar/sections/Locations/index.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/sections/Locations/index.tsx @@ -1,3 +1,4 @@ +import { keepPreviousData } from '@tanstack/react-query'; import clsx from 'clsx'; import { Link, useMatch } from 'react-router-dom'; import { @@ -18,7 +19,9 @@ import { SeeMore } from '../../SidebarLayout/SeeMore'; import { ContextMenu } from './ContextMenu'; export default function Locations() { - const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true }); + const locationsQuery = useLibraryQuery(['locations.list'], { + placeholderData: keepPreviousData + }); const locations = locationsQuery.data; const onlineLocations = useOnlineLocations(); diff --git a/interface/app/$libraryId/Layout/Sidebar/sections/Tags/index.tsx b/interface/app/$libraryId/Layout/Sidebar/sections/Tags/index.tsx index f251fc042..4131bbfe6 100644 --- a/interface/app/$libraryId/Layout/Sidebar/sections/Tags/index.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/sections/Tags/index.tsx @@ -1,3 +1,4 @@ +import { keepPreviousData } from '@tanstack/react-query'; import clsx from 'clsx'; import { NavLink, useMatch } from 'react-router-dom'; import { useLibraryQuery, type Tag } from '@sd/client'; @@ -11,7 +12,7 @@ import { SeeMore } from '../../SidebarLayout/SeeMore'; import { ContextMenu } from './ContextMenu'; export default function TagsSection() { - const result = useLibraryQuery(['tags.list'], { keepPreviousData: true }); + const result = useLibraryQuery(['tags.list'], { placeholderData: keepPreviousData }); const tags = result.data; const { t } = useLocale(); diff --git a/interface/app/$libraryId/Spacedrop/index.tsx b/interface/app/$libraryId/Spacedrop/index.tsx index 4c9a81342..6803a5283 100644 --- a/interface/app/$libraryId/Spacedrop/index.tsx +++ b/interface/app/$libraryId/Spacedrop/index.tsx @@ -93,7 +93,7 @@ export function Spacedrop({ triggerClose }: { triggerClose: () => void }) { }); const onDropped = (id: string, files: string[]) => { - if (doSpacedrop.isLoading) { + if (doSpacedrop.isPending) { toast.warning(t('spacedrop_already_progress')); return; } diff --git a/interface/app/$libraryId/debug/actors.tsx b/interface/app/$libraryId/debug/actors.tsx index a744583a6..cb14658c1 100644 --- a/interface/app/$libraryId/debug/actors.tsx +++ b/interface/app/$libraryId/debug/actors.tsx @@ -1,10 +1,10 @@ -import { inferSubscriptionResult } from '@oscartbeaumont-sd/rspc-client'; +import { inferSubscriptionResult } from '@spacedrive/rspc-client'; import { useMemo, useState } from 'react'; import { Procedures, useLibraryMutation, useLibrarySubscription } from '@sd/client'; import { Button } from '@sd/ui'; import { useRouteTitle } from '~/hooks/useRouteTitle'; -// @million-ignore +// million-ignore export const Component = () => { useRouteTitle('Actors'); @@ -46,10 +46,10 @@ function StartButton({ name }: { name: string }) { return ( ); } @@ -60,10 +60,10 @@ function StopButton({ name }: { name: string }) { return ( ); } diff --git a/interface/app/$libraryId/debug/cloud.tsx b/interface/app/$libraryId/debug/cloud.tsx index c3bf6ae35..fcec4339f 100644 --- a/interface/app/$libraryId/debug/cloud.tsx +++ b/interface/app/$libraryId/debug/cloud.tsx @@ -87,13 +87,13 @@ function Authenticated() { @@ -125,8 +125,8 @@ function HostedLocationsPlayground() { /> {/* TODO: Cleanup this mess + styles */} - {locations.status === 'loading' ?
Loading!
: null} - {locations.status !== 'loading' && locations.data?.length === 0 ? ( + {locations.status === 'pending' ?
Loading!
: null} + {locations.status !== 'pending' && locations.data?.length === 0 ? (
Looks like you don't have any!
) : (
@@ -137,7 +137,7 @@ function HostedLocationsPlayground() { variant="accent" size="sm" onClick={() => removeLocation.mutate(location.id)} - disabled={isLoading} + disabled={isPending} > Delete @@ -152,7 +152,7 @@ function HostedLocationsPlayground() { className="grow" value={path} onInput={(e) => setPath(e.currentTarget.value)} - disabled={isLoading} + disabled={isPending} />
diff --git a/interface/app/$libraryId/settings/client/backups.tsx b/interface/app/$libraryId/settings/client/backups.tsx index 69689b16f..0ebfe10e7 100644 --- a/interface/app/$libraryId/settings/client/backups.tsx +++ b/interface/app/$libraryId/settings/client/backups.tsx @@ -27,7 +27,7 @@ export const Component = () => { rightArea={
diff --git a/interface/app/$libraryId/settings/library/general.tsx b/interface/app/$libraryId/settings/library/general.tsx index d79b29363..a8b850923 100644 --- a/interface/app/$libraryId/settings/library/general.tsx +++ b/interface/app/$libraryId/settings/library/general.tsx @@ -109,7 +109,7 @@ export const Component = () => {
-// } -// {...form.register('masterPassword', { required: true })} -// /> - -// setShow((old) => ({ ...old, secretKey: !old.secretKey }))} -// size="icon" -// > -// -// -// } -// {...form.register('secretKey')} -// /> - -//
-// -//
-// -// ); -// }; diff --git a/interface/app/$libraryId/settings/library/keys/KeyViewerDialog.tsx b/interface/app/$libraryId/settings/library/keys/KeyViewerDialog.tsx deleted file mode 100644 index 9fe44cbac..000000000 --- a/interface/app/$libraryId/settings/library/keys/KeyViewerDialog.tsx +++ /dev/null @@ -1,160 +0,0 @@ -// import { Buffer } from 'buffer'; -// import { Clipboard } from '@phosphor-icons/react'; -// import { useState } from 'react'; -// import { slugFromHashingAlgo, useLibraryQuery } from '@sd/client'; -// import { Button, Dialog, Input, Select, SelectOption, UseDialogProps, useDialog } from '@sd/ui'; -// import { useZodForm } from '@sd/ui/src/forms'; -// import { KeyListSelectOptions } from '~/app/$libraryId/KeyManager/List'; - -// export const KeyUpdater = (props: { -// uuid: string; -// setKey: (value: string) => void; -// setEncryptionAlgo: (value: string) => void; -// setHashingAlgo: (value: string) => void; -// setContentSalt: (value: string) => void; -// }) => { -// useLibraryQuery(['keys.getKey', props.uuid], { -// onSuccess: (data) => { -// props.setKey(data); -// } -// }); - -// const keys = useLibraryQuery(['keys.list']); - -// const key = keys.data?.find((key) => key.uuid == props.uuid); - -// if (key) { -// props.setEncryptionAlgo(key?.algorithm); -// props.setHashingAlgo(slugFromHashingAlgo(key?.hashing_algorithm)); -// props.setContentSalt(Buffer.from(key.content_salt).toString('hex')); -// } - -// return <>; -// }; - -// export default (props: UseDialogProps) => { -// const keys = useLibraryQuery(['keys.list'], { -// onSuccess: (data) => { -// if (key === '' && data.length !== 0) { -// setKey(data[0]?.uuid ?? ''); -// } -// } -// }); - -// const [key, setKey] = useState(''); -// const [keyValue, setKeyValue] = useState(''); -// const [contentSalt, setContentSalt] = useState(''); -// const [encryptionAlgo, setEncryptionAlgo] = useState(''); -// const [hashingAlgo, setHashingAlgo] = useState(''); - -// return ( -// -// - -//
-//
-// Key -// -//
-//
-//
-//
-// Encryption -// -//
-//
-// Hashing -// -//
-//
-//
-//
-// Content Salt (hex) -// { -// navigator.clipboard.writeText(contentSalt); -// }} -// size="icon" -// > -// -// -// } -// /> -//
-//
-//
-//
-// Key Value -// { -// navigator.clipboard.writeText(keyValue); -// }} -// size="icon" -// > -// -// -// } -// /> -//
-//
-//
-// ); -// }; diff --git a/interface/app/$libraryId/settings/library/keys/MasterPasswordDialog.tsx b/interface/app/$libraryId/settings/library/keys/MasterPasswordDialog.tsx deleted file mode 100644 index 7b07a3135..000000000 --- a/interface/app/$libraryId/settings/library/keys/MasterPasswordDialog.tsx +++ /dev/null @@ -1,187 +0,0 @@ -// import { ArrowsClockwise, Clipboard, Eye, EyeSlash } from '@phosphor-icons/react'; -// import { useState } from 'react'; -// import { -// Algorithm, -// HASHING_ALGOS, -// HashingAlgoSlug, -// hashingAlgoSlugSchema, -// useLibraryMutation -// } from '@sd/client'; -// import { Button, Dialog, Input, Select, SelectOption, UseDialogProps, useDialog } from '@sd/ui'; -// import { useZodForm, z } from '@sd/ui/src/forms'; -// import { PasswordMeter, showAlertDialog } from '~/components'; -// import { generatePassword } from '~/util'; - -// const schema = z.object({ -// masterPassword: z.string(), -// masterPassword2: z.string(), -// encryptionAlgo: z.string(), -// hashingAlgo: hashingAlgoSlugSchema -// }); - -// export default (props: UseDialogProps) => { -// const changeMasterPassword = useLibraryMutation('keys.changeMasterPassword', { -// onSuccess: () => { -// showAlertDialog({ -// title: 'Success', -// value: 'Your master password was changed successfully' -// }); -// }, -// onError: () => { -// // this should never really happen -// showAlertDialog({ -// title: 'Master Password Change Error', -// value: 'There was an error while changing your master password.' -// }); -// } -// }); - -// const [show, setShow] = useState({ -// masterPassword: false, -// masterPassword2: false -// }); - -// const MP1CurrentEyeIcon = show.masterPassword ? EyeSlash : Eye; -// const MP2CurrentEyeIcon = show.masterPassword2 ? EyeSlash : Eye; - -// const form = useZodForm({ -// schema, -// defaultValues: { -// encryptionAlgo: 'XChaCha20Poly1305', -// hashingAlgo: 'Argon2id-s', -// masterPassword: '', -// masterPassword2: '' -// } -// }); - -// const onSubmit = form.handleSubmit((data) => { -// if (data.masterPassword !== data.masterPassword2) { -// showAlertDialog({ -// title: 'Error', -// value: 'Passwords are not the same, please try again.' -// }); -// } else { -// const hashing_algorithm = HASHING_ALGOS[data.hashingAlgo]; - -// return changeMasterPassword.mutateAsync({ -// algorithm: data.encryptionAlgo as Algorithm, -// hashing_algorithm, -// password: data.masterPassword -// }); -// } -// }); - -// return ( -// -// -// -// -// -//
-// } -// /> - -// -// setShow((old) => ({ ...old, masterPassword2: !old.masterPassword2 })) -// } -// size="icon" -// type="button" -// > -// -// -// } -// /> - -// - -//
-//
-// Encryption -// -//
-//
-// Hashing -// -//
-//
-// -// ); -// }; diff --git a/interface/app/$libraryId/settings/library/keys/index.tsx b/interface/app/$libraryId/settings/library/keys/index.tsx deleted file mode 100644 index e0c3bc314..000000000 --- a/interface/app/$libraryId/settings/library/keys/index.tsx +++ /dev/null @@ -1,293 +0,0 @@ -// import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; -// import { animated, useTransition } from '@react-spring/web'; -// import clsx from 'clsx'; -// import { Lock, Plus } from '@phosphor-icons/react'; -// import { PropsWithChildren, ReactNode, useState } from 'react'; -// import QRCode from 'react-qr-code'; -// import { useLibraryMutation, useLibraryQuery } from '@sd/client'; -// import { Button, PasswordInput, dialogManager } from '@sd/ui'; -// import { showAlertDialog } from '~/components/AlertDialog'; -// import { usePlatform } from '~/util/Platform'; -// import KeyList from '../../../KeyManager/List'; -// import KeyMounter from '../../../KeyManager/Mounter'; -// import { Heading } from '../../Layout'; -// import BackupRestoreDialog from './BackupRestoreDialog'; -// import KeyViewerDialog from './KeyViewerDialog'; -// import MasterPasswordDialog from './MasterPasswordDialog'; - -// interface Props extends DropdownMenu.MenuContentProps { -// trigger: React.ReactNode; -// transformOrigin?: string; -// disabled?: boolean; -// } - -// export const KeyMounterDropdown = ({ -// trigger, -// children, -// transformOrigin, -// className -// }: PropsWithChildren) => { -// const [open, setOpen] = useState(false); - -// const transitions = useTransition(open, { -// from: { -// opacity: 0, -// transform: `scale(0.9)`, -// transformOrigin: transformOrigin || 'top' -// }, -// enter: { opacity: 1, transform: 'scale(1)' }, -// leave: { opacity: -0.5, transform: 'scale(0.95)' }, -// config: { mass: 0.4, tension: 200, friction: 10 } -// }); - -// return ( -// -// {trigger} -// {transitions( -// (styles, show) => -// show && ( -// -// -// -// {children} -// -// -// -// ) -// )} -// -// ); -// }; - -// export const Component = () => { -// const platform = usePlatform(); -// const isUnlocked = useLibraryQuery(['keys.isUnlocked']); -// const keyringSk = useLibraryQuery(['keys.getSecretKey'], { initialData: '' }); // assume true by default, as it will often be the case. need to fix this with an rspc subscription+such -// const unlockKeyManager = useLibraryMutation('keys.unlockKeyManager', { -// onError: () => { -// showAlertDialog({ -// title: 'Unlock Error', -// value: 'The information provided to the key manager was incorrect' -// }); -// } -// }); - -// const unmountAll = useLibraryMutation('keys.unmountAll'); -// const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword'); -// const backupKeystore = useLibraryMutation('keys.backupKeystore'); -// const isKeyManagerUnlocking = useLibraryQuery(['keys.isKeyManagerUnlocking']); - -// const [masterPassword, setMasterPassword] = useState(''); -// const [secretKey, setSecretKey] = useState(''); // for the unlock form -// const [viewSecretKey, setViewSecretKey] = useState(false); // for the settings page - -// const keys = useLibraryQuery(['keys.list']); - -// const [enterSkManually, setEnterSkManually] = useState(keyringSk?.data === null); - -// if (!isUnlocked?.data) { -// return ( -//
-// setMasterPassword(e.target.value)} -// autoFocus -// placeholder="Master Password" -// className="mb-2" -// /> - -// {enterSkManually && ( -// setSecretKey(e.target.value)} -// placeholder="Secret Key" -// className="mb-2" -// /> -// )} - -// -// {!enterSkManually && ( -//
-//

setEnterSkManually(true)}> -// or enter secret key manually -//

-//
-// )} -//
-// ); -// } else { -// return ( -// <> -// -// -// -// -// -// } -// > -// -// -//
-// } -// /> - -// {isUnlocked && ( -//
-// -//
-// )} - -// {keyringSk?.data && ( -// <> -// -// {!viewSecretKey && ( -//
-// -//
-// )} -// {viewSecretKey && ( -//
{ -// keyringSk.data && navigator.clipboard.writeText(keyringSk.data); -// }} -// > -// <> -// -//

{keyringSk.data}

-// -//
-// )} -// -// )} - -// -//
-// -// -//
- -// -//
-// -// -//
-// -// ); -// } -// }; - -// interface SubheadingProps { -// title: string; -// rightArea?: ReactNode; -// } - -// const Subheading = (props: SubheadingProps) => ( -//
-//
-//

{props.title}

-//
-// {props.rightArea} -//
-// ); diff --git a/interface/app/$libraryId/settings/library/locations/$id.tsx b/interface/app/$libraryId/settings/library/locations/$id.tsx index 41fd59571..3bcec7b99 100644 --- a/interface/app/$libraryId/settings/library/locations/$id.tsx +++ b/interface/app/$libraryId/settings/library/locations/$id.tsx @@ -78,7 +78,7 @@ const EditLocationForm = () => { }, onSuccess: () => { form.reset(form.getValues()); - queryClient.invalidateQueries(['locations.list']); + queryClient.invalidateQueries({ queryKey: ['locations.list'] }); } }); diff --git a/interface/app/$libraryId/settings/library/saved-searches/index.tsx b/interface/app/$libraryId/settings/library/saved-searches/index.tsx index fa10daeb3..c3a6e19f7 100644 --- a/interface/app/$libraryId/settings/library/saved-searches/index.tsx +++ b/interface/app/$libraryId/settings/library/saved-searches/index.tsx @@ -108,7 +108,7 @@ function EditForm({ savedSearch, onDelete }: { savedSearch: SavedSearch; onDelet @@ -193,10 +193,10 @@ function StartButton({ name }: { name: string }) { return ( ); } @@ -208,10 +208,10 @@ function StopButton({ name }: { name: string }) { return ( ); } diff --git a/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx b/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx index 93032ecf4..60a54a93f 100644 --- a/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx +++ b/interface/app/$libraryId/settings/library/tags/CreateDialog.tsx @@ -28,7 +28,7 @@ export function useAssignItemsToTag() { const mutation = useLibraryMutation(['tags.assign'], { onSuccess: () => { submitPlausibleEvent({ event: { type: 'tagAssign' } }); - rspc.queryClient.invalidateQueries(['search.paths']); + rspc.queryClient.invalidateQueries({ queryKey: ['search.paths'] }); } }); diff --git a/interface/app/$libraryId/settings/node/libraries/DeleteDialog.tsx b/interface/app/$libraryId/settings/node/libraries/DeleteDialog.tsx index 7425a0df0..075fb5280 100644 --- a/interface/app/$libraryId/settings/node/libraries/DeleteDialog.tsx +++ b/interface/app/$libraryId/settings/node/libraries/DeleteDialog.tsx @@ -25,7 +25,7 @@ export default function DeleteLibraryDialog(props: Props) { try { await deleteLib.mutateAsync(props.libraryUuid); - queryClient.invalidateQueries(['library.list']); + queryClient.invalidateQueries({ queryKey: ['library.list'] }); if (platform.refreshMenuBar) platform.refreshMenuBar(); diff --git a/interface/app/$libraryId/settings/resources/changelog.tsx b/interface/app/$libraryId/settings/resources/changelog.tsx index ed257162f..e6965feb9 100644 --- a/interface/app/$libraryId/settings/resources/changelog.tsx +++ b/interface/app/$libraryId/settings/resources/changelog.tsx @@ -9,9 +9,10 @@ import { Heading } from '../Layout'; export const Component = () => { const platform = usePlatform(); const isDark = useIsDark(); - const changelog = useQuery(['changelog'], () => - fetch(`${platform.landingApiOrigin}/api/releases`).then((r) => r.json()) - ); + const changelog = useQuery({ + queryKey: ['changelog'], + queryFn: () => fetch(`${platform.landingApiOrigin}/api/releases`).then((r) => r.json()) + }); const { t } = useLocale(); diff --git a/interface/app/$libraryId/settings/resources/dependencies.tsx b/interface/app/$libraryId/settings/resources/dependencies.tsx deleted file mode 100644 index 2024f9252..000000000 --- a/interface/app/$libraryId/settings/resources/dependencies.tsx +++ /dev/null @@ -1,51 +0,0 @@ -// import { useQuery } from '@tanstack/react-query'; -// import { ScreenHeading } from '@sd/ui'; -// import { usePlatform } from '~/util/Platform'; - -// export const Component = () => { -// const frontEnd = useQuery( -// ['frontend-deps'], -// () => import('@sd/assets/deps/frontend-deps.json') -// ); -// const backEnd = useQuery(['backend-deps'], () => import('@sd/assets/deps/backend-deps.json')); -// const platform = usePlatform(); - -// return ( -//
-// Dependencies - -// {/* item has a LOT more data that we can display, i just went with the basics */} - -// Frontend Dependencies -//
-// {frontEnd.data && -// frontEnd.data?.default.map((item) => { -// return ( -// platform.openLink(item.url ?? '')}> -//
-//

-// {item.title.trimEnd().substring(0, 24) + -// (item.title.length > 24 ? '...' : '')} -//

-//
-//
-// ); -// })} -//
- -// Backend Dependencies -//
-// {backEnd.data && -// backEnd.data?.default.map((item) => { -// return ( -// platform.openLink(item.url ?? '')}> -//
-//

{item.title.trimEnd()}

-//
-//
-// ); -// })} -//
-//
-// ); -// }; diff --git a/interface/app/index.tsx b/interface/app/index.tsx index 3216925a1..2f70f199a 100644 --- a/interface/app/index.tsx +++ b/interface/app/index.tsx @@ -1,4 +1,4 @@ -import { initRspc, wsBatchLink, type AlphaClient } from '@oscartbeaumont-sd/rspc-client/src/v2'; +import { initRspc, wsBatchLink, type AlphaClient } from '@spacedrive/rspc-client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useEffect, useMemo, useState } from 'react'; import { diff --git a/interface/app/onboarding/join-library.tsx b/interface/app/onboarding/join-library.tsx index d4bbed977..1baf7f967 100644 --- a/interface/app/onboarding/join-library.tsx +++ b/interface/app/onboarding/join-library.tsx @@ -55,7 +55,7 @@ function CloudLibraries() { {cloudLibrary.name} diff --git a/interface/components/Devtools.tsx b/interface/components/Devtools.tsx index acb72e803..952019859 100644 --- a/interface/components/Devtools.tsx +++ b/interface/components/Devtools.tsx @@ -1,4 +1,3 @@ -import { defaultContext } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { useDebugState } from '@sd/client'; @@ -7,18 +6,9 @@ export const Devtools = () => { return ( <> - {debugState.reactQueryDevtools !== 'disabled' ? ( - - ) : null} + {debugState.reactQueryDevtools && ( + + )} ); }; diff --git a/interface/components/Sparkles.tsx b/interface/components/Sparkles.tsx index ec6b73ffa..2b1dc612d 100644 --- a/interface/components/Sparkles.tsx +++ b/interface/components/Sparkles.tsx @@ -38,7 +38,8 @@ type SparklesProps = { children: React.ReactNode; }; -const Sparkles = ({ color = DEFAULT_COLOR, children, ...props }: SparklesProps) => { +// million-ignore +const Sparkles = ({ color = DEFAULT_COLOR, children }: SparklesProps) => { const [sparkles, setSparkles] = useState(() => { return range(3).map(() => generateSparkle(color)); }); @@ -60,7 +61,7 @@ const Sparkles = ({ color = DEFAULT_COLOR, children, ...props }: SparklesProps) ); return ( - + {sparkles.map((sparkle) => ( { + return useSuspenseQuery({ + queryKey: ['userDirs', 'home'], + queryFn: () => { if (platform.userHomeDir) return platform.userHomeDir(); else return null; - }, - { suspense: true } - ); + } + }); } diff --git a/interface/hooks/useOperatingSystem.ts b/interface/hooks/useOperatingSystem.ts index c5797661e..acba5a8c6 100644 --- a/interface/hooks/useOperatingSystem.ts +++ b/interface/hooks/useOperatingSystem.ts @@ -17,17 +17,15 @@ export function guessOperatingSystem(): OperatingSystem { // Setting `realOs` to true will return a best guess of the underlying operating system instead of 'browser'. export function useOperatingSystem(realOs?: boolean): OperatingSystem { const platform = usePlatform(); - const { data } = useQuery( - ['_tauri', 'platform'], - async () => { + const { data } = useQuery({ + queryKey: ['_tauri', 'platform'], + queryFn: async () => { return platform.getOs ? await platform.getOs() : guessOperatingSystem(); }, - { - // Here we guess the users operating system from the user agent for the first render. - initialData: guessOperatingSystem, - enabled: platform.getOs !== undefined - } - ); + // Here we guess the users operating system from the user agent for the first render. + initialData: guessOperatingSystem, + enabled: platform.getOs !== undefined + }); return platform.platform === 'web' && !realOs ? 'browser' : data; } diff --git a/interface/package.json b/interface/package.json index c3a528139..0b3b91acf 100644 --- a/interface/package.json +++ b/interface/package.json @@ -26,10 +26,10 @@ "@sd/client": "workspace:*", "@sd/ui": "workspace:*", "@sentry/browser": "^7.74.1", - "@tanstack/react-query": "^4.36.1", - "@tanstack/react-query-devtools": "^4.36.1", - "@tanstack/react-table": "^8.10.7", - "@tanstack/react-virtual": "3.0.0-beta.66", + "@tanstack/react-query": "^5.59", + "@tanstack/react-query-devtools": "^5.59", + "@tanstack/react-table": "^8.20.5", + "@tanstack/react-virtual": "3.10.8", "@total-typescript/ts-reset": "^0.5.1", "@virtual-grid/react": "^2.0.2", "class-variance-authority": "^0.7.0", @@ -69,7 +69,7 @@ "use-debounce": "^9.0.4", "use-resize-observer": "^9.1.0", "uuid": "^9.0.1", - "valtio": "^1.11.2" + "valtio": "^2.0" }, "devDependencies": { "@sd/config": "workspace:*", diff --git a/interface/util/useTraceUpdate.tsx b/interface/util/useTraceUpdate.tsx new file mode 100644 index 000000000..c40ebc326 --- /dev/null +++ b/interface/util/useTraceUpdate.tsx @@ -0,0 +1,28 @@ +import { useEffect, useRef } from 'react'; + +/** + * DO NOT DELETE THIS HOOK + * It probably isn't used in the codebase, but it's a useful debugging tool. + */ +export function useTraceUpdate(name: string, props: object | null) { + const prev = useRef<{ [key: string]: any } | null>(props); + useEffect(() => { + const { current } = prev; + if (props == null) { + console.log(`Change ${name} to null`); + } else if (current == null) { + console.log(`Change ${name} from null to`, props); + } else { + const changedProps = Object.entries(props).reduce((ps: any, [k, v]) => { + if (current[k] !== v) { + ps[k] = [current[k], v]; + } + return ps; + }, {}); + if (Object.keys(changedProps).length > 0) { + console.log(`Changed ${name}:`, changedProps); + } + } + prev.current = props; + }); +} diff --git a/package.json b/package.json index 80924fac7..393d7f33c 100644 --- a/package.json +++ b/package.json @@ -73,5 +73,5 @@ "eslintConfig": { "root": true }, - "packageManager": "pnpm@9.9.0" + "packageManager": "pnpm@9.12.1" } diff --git a/packages/client/package.json b/packages/client/package.json index 68f7f1a07..f2db671f6 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -11,11 +11,11 @@ "typecheck": "tsc -b" }, "dependencies": { - "@oscartbeaumont-sd/rspc-client": "github:spacedriveapp/rspc#path:packages/client&bc882f4724", - "@oscartbeaumont-sd/rspc-react": "github:spacedriveapp/rspc#path:packages/react&bc882f4724", + "@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495", + "@spacedrive/rspc-react": "github:spacedriveapp/rspc#path:packages/react&6a77167495", "@solid-primitives/deep": "^0.2.4", - "@tanstack/react-query": "^4.36.1", - "@tanstack/solid-query": "^5.17.9", + "@tanstack/react-query": "^5.59", + "@tanstack/solid-query": "^5.59", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", @@ -23,7 +23,7 @@ "plausible-tracker": "^0.3.8", "react-hook-form": "^7.47.0", "solid-js": "^1.8.8", - "zod": "~3.22.4" + "zod": "^3.23" }, "devDependencies": { "@sd/config": "workspace:*", diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 2935c0951..bc4c2fe8c 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually. +// This file was generated by [rspc](https://github.com/spacedriveapp/rspc). Do not edit this file manually. export type Procedures = { queries: diff --git a/packages/client/src/explorer/index.ts b/packages/client/src/explorer/index.ts index 34dcf4dce..9703b13af 100644 --- a/packages/client/src/explorer/index.ts +++ b/packages/client/src/explorer/index.ts @@ -1,5 +1,4 @@ export * from './useExplorerInfiniteQuery'; -export * from './usePathsInfiniteQuery'; export * from './usePathsOffsetInfiniteQuery'; export * from './usePathsExplorerQuery'; export * from './useObjectsInfiniteQuery'; diff --git a/packages/client/src/explorer/useExplorerInfiniteQuery.ts b/packages/client/src/explorer/useExplorerInfiniteQuery.ts index 1b3c25ce5..80b4c5767 100644 --- a/packages/client/src/explorer/useExplorerInfiniteQuery.ts +++ b/packages/client/src/explorer/useExplorerInfiniteQuery.ts @@ -6,5 +6,4 @@ import { Ordering } from './index'; export type UseExplorerInfiniteQueryArgs = { arg: TArg; order: TOrder | null; - onSuccess?: () => void; -} & Pick>, 'enabled' | 'suspense'>; +}; diff --git a/packages/client/src/explorer/useExplorerQuery.ts b/packages/client/src/explorer/useExplorerQuery.ts index b41c9d247..ad7630b5d 100644 --- a/packages/client/src/explorer/useExplorerQuery.ts +++ b/packages/client/src/explorer/useExplorerQuery.ts @@ -1,13 +1,16 @@ -import { UseInfiniteQueryResult, UseQueryResult } from '@tanstack/react-query'; +import { InfiniteData, UseInfiniteQueryResult, UseQueryResult } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; import { SearchData } from '../core'; export function useExplorerQuery( - query: UseInfiniteQueryResult>, + query: UseInfiniteQueryResult>>, count: UseQueryResult ) { - const items = useMemo(() => query.data?.pages.flatMap((d) => d.items) ?? null, [query.data]); + const items = useMemo( + () => query.data?.pages.flatMap((data) => data.items) ?? null, + [query.data] + ); const loadMore = useCallback(() => { if (query.hasNextPage && !query.isFetchingNextPage) { diff --git a/packages/client/src/explorer/useObjectsInfiniteQuery.ts b/packages/client/src/explorer/useObjectsInfiniteQuery.ts index 6e758e1ba..c8680a2b8 100644 --- a/packages/client/src/explorer/useObjectsInfiniteQuery.ts +++ b/packages/client/src/explorer/useObjectsInfiniteQuery.ts @@ -8,8 +8,7 @@ import { UseExplorerInfiniteQueryArgs } from './useExplorerInfiniteQuery'; export function useObjectsInfiniteQuery({ arg, - order, - ...args + order }: UseExplorerInfiniteQueryArgs) { const { library } = useLibraryContext(); const ctx = useRspcLibraryContext(); @@ -21,25 +20,23 @@ export function useObjectsInfiniteQuery({ const query = useInfiniteQuery({ queryKey: ['search.objects', { library_id: library.uuid, arg }] as const, queryFn: ({ pageParam, queryKey: [_, { arg }] }) => { - const cItem: Extract = pageParam; - let orderAndPagination: (typeof arg)['orderAndPagination']; - if (!cItem) { + if (!pageParam || pageParam.type !== 'Object') { if (order) orderAndPagination = { orderOnly: order }; } else { let cursor: ObjectCursor | undefined; if (!order) cursor = 'none'; - else if (cItem) { + else if (pageParam) { switch (order.field) { case 'kind': { - const data = cItem.item.kind; + const data = pageParam.item.kind; if (data !== null) cursor = { kind: { order: order.value, data } }; break; } case 'dateAccessed': { - const data = cItem.item.date_accessed; + const data = pageParam.item.date_accessed; if (data !== null) cursor = { dateAccessed: { order: order.value, data } }; break; @@ -47,18 +44,18 @@ export function useObjectsInfiniteQuery({ } } - if (cursor) orderAndPagination = { cursor: { cursor, id: cItem.item.id } }; + if (cursor) orderAndPagination = { cursor: { cursor, id: pageParam.item.id } }; } arg.orderAndPagination = orderAndPagination; return ctx.client.query(['search.objects', arg]); }, + initialPageParam: undefined as ExplorerItem | undefined, getNextPageParam: (lastPage) => { if (lastPage.items.length < arg.take) return undefined; else return lastPage.items[arg.take - 1]; - }, - ...args + } }); return query; diff --git a/packages/client/src/explorer/useObjectsOffsetInfiniteQuery.ts b/packages/client/src/explorer/useObjectsOffsetInfiniteQuery.ts index 7bb20c9e4..3580bcf67 100644 --- a/packages/client/src/explorer/useObjectsOffsetInfiniteQuery.ts +++ b/packages/client/src/explorer/useObjectsOffsetInfiniteQuery.ts @@ -8,8 +8,7 @@ import { UseExplorerInfiniteQueryArgs } from './useExplorerInfiniteQuery'; export function useObjectsOffsetInfiniteQuery({ arg, - order, - ...args + order }: UseExplorerInfiniteQueryArgs) { const { library } = useLibraryContext(); const ctx = useRspcLibraryContext(); @@ -40,10 +39,10 @@ export function useObjectsOffsetInfiniteQuery({ return { ...result, offset: pageParam, arg }; }, + initialPageParam: 0, getNextPageParam: ({ items, offset, arg }) => { if (items.length >= arg.take) return (offset ?? 0) + 1; - }, - ...args + } }); return query; diff --git a/packages/client/src/explorer/usePathsExplorerQuery.ts b/packages/client/src/explorer/usePathsExplorerQuery.ts index f64f56504..4894fbc74 100644 --- a/packages/client/src/explorer/usePathsExplorerQuery.ts +++ b/packages/client/src/explorer/usePathsExplorerQuery.ts @@ -8,8 +8,6 @@ export function usePathsExplorerQuery(props: { order: FilePathOrder | null; enabled?: boolean; suspense?: boolean; - /** This callback will fire any time the query successfully fetches new data. (NOTE: This will be removed on the next major version (react-query)) */ - onSuccess?: () => void; }) { const query = usePathsOffsetInfiniteQuery(props); diff --git a/packages/client/src/explorer/usePathsInfiniteQuery.ts b/packages/client/src/explorer/usePathsInfiniteQuery.ts deleted file mode 100644 index c28a44b8a..000000000 --- a/packages/client/src/explorer/usePathsInfiniteQuery.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; - -import { - ExplorerItem, - FilePathCursorVariant, - FilePathObjectCursor, - FilePathOrder, - FilePathSearchArgs -} from '../core'; -import { useLibraryContext } from '../hooks'; -import { useRspcLibraryContext } from '../rspc'; -import { UseExplorerInfiniteQueryArgs } from './useExplorerInfiniteQuery'; - -export function usePathsInfiniteQuery({ - arg, - order, - onSuccess, - ...args -}: UseExplorerInfiniteQueryArgs) { - const { library } = useLibraryContext(); - const ctx = useRspcLibraryContext(); - - if (order) { - arg.orderAndPagination = { orderOnly: order }; - if (arg.orderAndPagination.orderOnly.field === 'sizeInBytes') delete arg.take; - } - - const query = useInfiniteQuery({ - queryKey: ['search.paths', { library_id: library.uuid, arg }] as const, - queryFn: async ({ pageParam, queryKey: [_, { arg }] }) => { - const cItem: Extract = pageParam; - - let orderAndPagination: (typeof arg)['orderAndPagination']; - - if (!cItem) { - if (order) orderAndPagination = { orderOnly: order }; - } else { - let variant: FilePathCursorVariant | undefined; - - if (!order) variant = 'none'; - else if (cItem) { - switch (order.field) { - case 'name': { - const data = cItem.item.name; - if (data !== null) - variant = { - name: { order: order.value, data } - }; - break; - } - case 'sizeInBytes': { - variant = { sizeInBytes: order.value }; - break; - } - case 'dateCreated': { - const data = cItem.item.date_created; - if (data !== null) - variant = { - dateCreated: { order: order.value, data } - }; - break; - } - case 'dateModified': { - const data = cItem.item.date_modified; - if (data !== null) - variant = { - dateModified: { order: order.value, data } - }; - break; - } - case 'dateIndexed': { - const data = cItem.item.date_indexed; - if (data !== null) - variant = { - dateIndexed: { order: order.value, data } - }; - break; - } - case 'object': { - const object = cItem.item.object; - if (!object) break; - - let objectCursor: FilePathObjectCursor | undefined; - - switch (order.value.field) { - case 'dateAccessed': { - const data = object.date_accessed; - if (data !== null) - objectCursor = { - dateAccessed: { order: order.value.value, data } - }; - break; - } - case 'kind': { - const data = object.kind; - if (data !== null) - objectCursor = { - kind: { order: order.value.value, data } - }; - break; - } - } - - if (objectCursor) variant = { object: objectCursor }; - - break; - } - } - } - - if (cItem.item.is_dir === null) throw new Error(); - - if (variant) - orderAndPagination = { - cursor: { cursor: { variant, isDir: cItem.item.is_dir }, id: cItem.item.id } - }; - } - - arg.orderAndPagination = orderAndPagination; - - const result = await ctx.client.query(['search.paths', arg]); - return result; - }, - getNextPageParam: (lastPage) => { - if (arg.take === null || arg.take === undefined) return undefined; - if (lastPage.items.length < arg.take) return undefined; - else return lastPage.items[arg.take - 1]; - }, - onSuccess, - ...args - }); - - return query; -} diff --git a/packages/client/src/explorer/usePathsOffsetInfiniteQuery.ts b/packages/client/src/explorer/usePathsOffsetInfiniteQuery.ts index 52880b489..95d6fba3b 100644 --- a/packages/client/src/explorer/usePathsOffsetInfiniteQuery.ts +++ b/packages/client/src/explorer/usePathsOffsetInfiniteQuery.ts @@ -7,9 +7,7 @@ import { UseExplorerInfiniteQueryArgs } from './useExplorerInfiniteQuery'; export function usePathsOffsetInfiniteQuery({ arg, - order, - onSuccess, - ...args + order }: UseExplorerInfiniteQueryArgs) { const take = arg.take ?? 100; @@ -49,11 +47,10 @@ export function usePathsOffsetInfiniteQuery({ return { ...result, offset: pageParam, arg }; }, + initialPageParam: 0, getNextPageParam: ({ items, offset, arg }) => { if (items.length >= arg.take) return (offset ?? 0) + 1; - }, - onSuccess, - ...args + } }); return query; diff --git a/packages/client/src/hooks/useClientContext.tsx b/packages/client/src/hooks/useClientContext.tsx index 26c870c76..f66825a82 100644 --- a/packages/client/src/hooks/useClientContext.tsx +++ b/packages/client/src/hooks/useClientContext.tsx @@ -1,5 +1,6 @@ -import { AlphaClient } from '@oscartbeaumont-sd/rspc-client/src/v2'; -import { createContext, PropsWithChildren, useContext, useMemo } from 'react'; +import { AlphaClient } from '@spacedrive/rspc-client'; +import { keepPreviousData } from '@tanstack/react-query'; +import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react'; import { LibraryConfigWrapped, Procedures } from '../core'; import { valtioPersist } from '../lib'; @@ -9,8 +10,8 @@ import { useBridgeQuery } from '../rspc'; const libraryCacheLocalStorageKey = 'sd-library-list3'; // number is because the format of this underwent breaking changes export const useCachedLibraries = () => { - const result = useBridgeQuery(['library.list'], { - keepPreviousData: true, + const query = useBridgeQuery(['library.list'], { + placeholderData: keepPreviousData, initialData: () => { const cachedData = localStorage.getItem(libraryCacheLocalStorageKey); @@ -24,14 +25,15 @@ export const useCachedLibraries = () => { } return undefined; - }, - onSuccess: (data) => { - if (data.length > 0) - localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(data)); } }); - return result; + useEffect(() => { + if ((query.data?.length ?? 0) > 0) + localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(query.data)); + }, [query.data]); + + return query; }; export async function getCachedLibraries(client: AlphaClient) { diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 4a96e699f..dc0c5d41a 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,4 +1,4 @@ -import { Link } from '@oscartbeaumont-sd/rspc-client/src/v2'; +import { Link } from '@spacedrive/rspc-client'; declare global { // eslint-disable-next-line diff --git a/packages/client/src/lib/humanizeSize.ts b/packages/client/src/lib/humanizeSize.ts index 45a2cd167..654fe000f 100644 --- a/packages/client/src/lib/humanizeSize.ts +++ b/packages/client/src/lib/humanizeSize.ts @@ -146,3 +146,10 @@ export const humanizeSize = ( } }; }; + +export const compareHumanizedSizes = ( + size1: ReturnType, + size2: ReturnType +): boolean => { + return size1.bytes === size2.bytes && size1.unit === size2.unit && size1.value === size2.value; +}; diff --git a/packages/client/src/rspc-cursed.ts b/packages/client/src/rspc-cursed.ts index 774dcfcfb..ff1c026ef 100644 --- a/packages/client/src/rspc-cursed.ts +++ b/packages/client/src/rspc-cursed.ts @@ -1,5 +1,5 @@ -import { _inferProcedureHandlerInput, inferProcedureResult } from '@oscartbeaumont-sd/rspc-client'; -import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; +import { _inferProcedureHandlerInput, inferProcedureResult } from '@spacedrive/rspc-client'; +import { UseQueryOptions, useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query'; import { useRef } from 'react'; import { Procedures } from './core'; @@ -26,19 +26,20 @@ export function useUnsafeStreamedQuery< TData = inferProcedureResult >( keyAndInput: [K, ..._inferProcedureHandlerInput], - opts: UseQueryOptions & { + opts: Omit, 'queryKey'> & { onBatch(item: TData): void; } -): UseQueryResult & { streaming: TData[] } { +): UseSuspenseQueryResult & { streaming: TData[] } { const data = useRef([]); const rspc = useRspcContext(); // TODO: The normalised cache might cleanup nodes for this query before it's finished streaming. We need a global mutex on the cleanup routine. - const query = useQuery({ + const query = useSuspenseQuery({ + ...opts, queryKey: keyAndInput, queryFn: ({ signal }) => - new Promise((resolve) => { + new Promise((resolve) => { permits += 1; try { @@ -48,7 +49,7 @@ export function useUnsafeStreamedQuery< if (item === null || item === undefined) return; if (typeof item === 'object' && '__stream_complete' in item) { - resolve(data.current as any); + resolve(data.current); return; } @@ -60,8 +61,7 @@ export function useUnsafeStreamedQuery< } finally { permits -= 1; } - }), - ...opts + }) }); return { diff --git a/packages/client/src/rspc.tsx b/packages/client/src/rspc.tsx index 40ec57782..579eacd7f 100644 --- a/packages/client/src/rspc.tsx +++ b/packages/client/src/rspc.tsx @@ -1,6 +1,5 @@ -import { ProcedureDef } from '@oscartbeaumont-sd/rspc-client'; -import { AlphaRSPCError, initRspc } from '@oscartbeaumont-sd/rspc-client/src/v2'; -import { Context, createReactQueryHooks } from '@oscartbeaumont-sd/rspc-react/src/v2'; +import { initRspc, ProcedureDef, RSPCError } from '@spacedrive/rspc-client'; +import { Context, createReactQueryHooks } from '@spacedrive/rspc-react/src/v2'; import { QueryClient } from '@tanstack/react-query'; import { createContext, PropsWithChildren, useContext } from 'react'; import { match, P } from 'ts-pattern'; @@ -102,7 +101,7 @@ export function useInvalidateQuery() { for (const op of ops) { match(op) .with({ type: 'single', data: P.select() }, (op) => { - let key: any[] = [op.key]; + let key: unknown[] = [op.key]; if (op.arg !== null) { key = key.concat(op.arg); } @@ -110,7 +109,7 @@ export function useInvalidateQuery() { if (op.result !== null) { context.queryClient.setQueryData(key, op.result); } else { - context.queryClient.invalidateQueries(key); + context.queryClient.invalidateQueries({ queryKey: key }); } }) .with({ type: 'all' }, (op) => { @@ -124,6 +123,6 @@ export function useInvalidateQuery() { // TODO: Remove/fix this when rspc typesafe errors are working export function extractInfoRSPCError(error: unknown) { - if (!(error instanceof AlphaRSPCError)) return null; + if (!(error instanceof RSPCError)) return null; return error; } diff --git a/packages/client/src/solid/index.ts b/packages/client/src/solid/index.ts index dad153583..3a7aaf513 100644 --- a/packages/client/src/solid/index.ts +++ b/packages/client/src/solid/index.ts @@ -2,7 +2,6 @@ export * from './createPersistedMutable'; export * from './react'; export * from './solid.solid'; export * from './useObserver'; -export * from './useUniversalQuery'; export * from './useSolidStore'; export { InteropProviderReact } from './portals'; export { createSharedContext } from './context'; diff --git a/packages/client/src/solid/solid.solid.tsx b/packages/client/src/solid/solid.solid.tsx index 8036b6e07..dfe24b907 100644 --- a/packages/client/src/solid/solid.solid.tsx +++ b/packages/client/src/solid/solid.solid.tsx @@ -1,5 +1,4 @@ /** @jsxImportSource solid-js */ - import { trackDeep } from '@solid-primitives/deep'; import { createElement, StrictMode, type FunctionComponent } from 'react'; import { createPortal } from 'react-dom'; diff --git a/packages/client/src/solid/useUniversalQuery.ts b/packages/client/src/solid/useUniversalQuery.ts deleted file mode 100644 index 2e9435448..000000000 --- a/packages/client/src/solid/useUniversalQuery.ts +++ /dev/null @@ -1,14 +0,0 @@ -// import { useQuery } from '@tanstack/react-query'; -// import { createQuery } from '@tanstack/solid-query'; - -// import { insideReactRender } from './internal'; - -// export function useUniversalQuery() { -// if (insideReactRender()) { -// useQuery(); -// } else { -// createQuery(); -// } -// } - -export {}; // TODO diff --git a/packages/client/src/stores/auth.ts b/packages/client/src/stores/auth.ts index d261fc845..a494854c0 100644 --- a/packages/client/src/stores/auth.ts +++ b/packages/client/src/stores/auth.ts @@ -1,4 +1,4 @@ -import { RSPCError } from '@oscartbeaumont-sd/rspc-client'; +import { RSPCError } from '@spacedrive/rspc-client'; import { createMutable } from 'solid-js/store'; import { nonLibraryClient } from '../rspc'; diff --git a/packages/client/src/stores/debugState.ts b/packages/client/src/stores/debugState.ts index 23fd751f4..7bfc80855 100644 --- a/packages/client/src/stores/debugState.ts +++ b/packages/client/src/stores/debugState.ts @@ -6,7 +6,7 @@ import { createPersistedMutable, useSolidStore } from '../solid'; export interface DebugState { enabled: boolean; rspcLogger: boolean; - reactQueryDevtools: 'enabled' | 'disabled' | 'invisible'; + reactQueryDevtools: boolean; shareFullTelemetry: boolean; // used for sending telemetry even if the app is in debug mode telemetryLogging: boolean; } @@ -16,7 +16,7 @@ export const debugState = createPersistedMutable( createMutable({ enabled: globalThis.isDev, rspcLogger: false, - reactQueryDevtools: globalThis.isDev ? 'invisible' : 'enabled', + reactQueryDevtools: false, shareFullTelemetry: false, telemetryLogging: false }) diff --git a/packages/config/vite/narrowSolidPlugin.ts b/packages/config/vite/narrowSolidPlugin.ts index 425970ff5..1791ff0fd 100644 --- a/packages/config/vite/narrowSolidPlugin.ts +++ b/packages/config/vite/narrowSolidPlugin.ts @@ -13,10 +13,14 @@ export interface NarrowSolidPluginOptions extends Partial { export function narrowSolidPlugin({ include, exclude, ...rest }: NarrowSolidPluginOptions = {}) { const plugin = solidPlugin(rest); - const originalConfig = plugin.config!.bind(plugin); + const originalConfig = + typeof plugin.config == 'function' + ? (plugin.config.bind(plugin) as typeof plugin.config) + : plugin.config; const filter = createFilter(include, exclude); plugin.config = (...args) => { - const baseConfig = originalConfig(...args); + const baseConfig = + typeof originalConfig == 'function' ? originalConfig?.(...args) : originalConfig; return { ...baseConfig, esbuild: { diff --git a/packages/ui/package.json b/packages/ui/package.json index fc2a83e9b..9cdeef6fd 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -44,7 +44,7 @@ "react-router-dom": "=6.20.1", "sonner": "^1.0.3", "use-debounce": "^9.0.4", - "zod": "~3.22.4" + "zod": "^3.23" }, "devDependencies": { "@babel/core": "^7.24.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d975ad8f5..12c81db05 100644 Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ diff --git a/scripts/list-dup-deps.sh b/scripts/list-dup-deps.sh deleted file mode 100755 index 97c4023ff..000000000 --- a/scripts/list-dup-deps.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -_root="$(CDPATH='' cd "$(dirname "$0")" && pwd -P)" - -grep -Po '^\s+"[\w-]+\s+\d(\.\d+)*[^"]*"' "${_root}/../Cargo.lock" \ - | xargs printf '%s\n' \ - | sort -u -k 1b,2V diff --git a/scripts/preprep.mjs b/scripts/preprep.mjs index 7ffa7a06a..7e0799e03 100755 --- a/scripts/preprep.mjs +++ b/scripts/preprep.mjs @@ -1,5 +1,4 @@ #!/usr/bin/env node - import * as fs from 'node:fs/promises' import * as path from 'node:path' import { env, exit, umask } from 'node:process' @@ -9,11 +8,11 @@ import { extractTo } from 'archive-wasm/src/fs.mjs' import * as _mustache from 'mustache' import { parse as parseTOML } from 'smol-toml' -import { getConst, NATIVE_DEPS_URL, NATIVE_DEPS_ASSETS } from './utils/consts.mjs' +import { getConst, NATIVE_DEPS_ASSETS, NATIVE_DEPS_URL } from './utils/consts.mjs' import { get } from './utils/fetch.mjs' import { getMachineId } from './utils/machineId.mjs' import { getRustTargetList } from './utils/rustup.mjs' -import { symlinkSharedLibsMacOS, symlinkSharedLibsLinux } from './utils/shared.mjs' +import { symlinkSharedLibsLinux, symlinkSharedLibsMacOS } from './utils/shared.mjs' import { spinTask } from './utils/spinner.mjs' import { which } from './utils/which.mjs' diff --git a/scripts/tauri.mjs b/scripts/tauri.mjs index 0eed0d73f..46e5f5640 100755 --- a/scripts/tauri.mjs +++ b/scripts/tauri.mjs @@ -1,8 +1,7 @@ #!/usr/bin/env node - import * as fs from 'node:fs/promises' import * as path from 'node:path' -import { env, exit, umask, platform } from 'node:process' +import { env, exit, platform, umask } from 'node:process' import { setTimeout } from 'node:timers/promises' import { fileURLToPath } from 'node:url' diff --git a/scripts/utils/fetch.mjs b/scripts/utils/fetch.mjs index cf8cd6c41..5907e418e 100644 --- a/scripts/utils/fetch.mjs +++ b/scripts/utils/fetch.mjs @@ -4,7 +4,7 @@ import { env } from 'node:process' import { fileURLToPath } from 'node:url' import { getSystemProxy } from 'os-proxy-config' -import { fetch, Headers, Agent, ProxyAgent } from 'undici' +import { Agent, fetch, Headers, ProxyAgent } from 'undici' const CONNECT_TIMEOUT = 5 * 60 * 1000 const __debug = env.NODE_ENV === 'debug'