diff --git a/core/src/api/files.rs b/core/src/api/files.rs index a292bc367..df3f274b1 100644 --- a/core/src/api/files.rs +++ b/core/src/api/files.rs @@ -5,14 +5,16 @@ use crate::{ library::Library, location::{ file_path_helper::{ - file_path_to_isolate, file_path_to_isolate_with_id, FilePathError, IsolatedFilePathData, + file_path_to_full_path, file_path_to_isolate, file_path_to_isolate_with_id, + FilePathError, IsolatedFilePathData, }, get_location_path_from_location_id, LocationError, }, object::{ fs::{ copy::FileCopierJobInit, cut::FileCutterJobInit, delete::FileDeleterJobInit, - erase::FileEraserJobInit, + erase::FileEraserJobInit, error::FileSystemJobsError, + find_available_filename_for_duplicate, }, media::{ media_data_extractor::{ @@ -31,7 +33,7 @@ use sd_media_metadata::MediaMetadata; use std::{ ffi::OsString, - path::{Path, PathBuf}, + path::{Path, PathBuf, MAIN_SEPARATOR, MAIN_SEPARATOR_STR}, str::FromStr, sync::Arc, }; @@ -47,6 +49,8 @@ use tracing::{error, warn}; use super::{Ctx, R}; +const UNTITLED_FOLDER_STR: &str = "Untitled Folder"; + pub(crate) fn mount() -> AlphaRouter { R.router() .procedure("get", { @@ -194,6 +198,53 @@ pub(crate) fn mount() -> AlphaRouter { Ok(()) }) }) + .procedure("createFolder", { + #[derive(Type, Deserialize)] + pub struct CreateFolderArgs { + pub location_id: location::id::Type, + pub sub_path: Option, + pub name: Option, + } + R.with2(library()).mutation( + |(_, library), + CreateFolderArgs { + location_id, + sub_path, + name, + }: CreateFolderArgs| async move { + let mut path = + get_location_path_from_location_id(&library.db, location_id).await?; + + if let Some(sub_path) = sub_path + .as_ref() + .and_then(|sub_path| sub_path.strip_prefix("/").ok()) + { + path.push(sub_path); + } + + path.push(name.as_deref().unwrap_or(UNTITLED_FOLDER_STR)); + + dbg!(&path); + + create_directory(path, &library).await + }, + ) + }) + .procedure("createEphemeralFolder", { + #[derive(Type, Deserialize)] + pub struct CreateEphemeralFolderArgs { + pub path: PathBuf, + pub name: Option, + } + R.with2(library()).mutation( + |(_, library), + CreateEphemeralFolderArgs { mut path, name }: CreateEphemeralFolderArgs| async move { + path.push(name.as_deref().unwrap_or(UNTITLED_FOLDER_STR)); + + create_directory(path, &library).await + }, + ) + }) .procedure("updateAccessTime", { R.with2(library()) .mutation(|(_, library), ids: Vec| async move { @@ -692,3 +743,43 @@ pub(crate) fn mount() -> AlphaRouter { ) }) } + +async fn create_directory( + mut target_path: PathBuf, + library: &Library, +) -> Result { + match fs::metadata(&target_path).await { + Ok(metadata) if metadata.is_dir() => { + target_path = find_available_filename_for_duplicate(&target_path).await?; + } + Ok(_) => { + return Err(FileSystemJobsError::WouldOverwrite(target_path.into_boxed_path()).into()) + } + Err(e) if e.kind() == io::ErrorKind::NotFound => { + // Everything is awesome! + } + Err(e) => { + return Err(FileIOError::from(( + target_path, + e, + "Failed to access file system and get metadata on directory to be created", + )) + .into()) + } + }; + + fs::create_dir(&target_path) + .await + .map_err(|e| FileIOError::from((&target_path, e, "Failed to create directory")))?; + + println!("Created directory: {}", target_path.display()); + + invalidate_query!(library, "search.objects"); + invalidate_query!(library, "search.paths"); + + Ok(target_path + .file_name() + .expect("Failed to get file name") + .to_string_lossy() + .to_string()) +} diff --git a/core/src/job/manager.rs b/core/src/job/manager.rs index f26f5ff59..adf16d8d0 100644 --- a/core/src/job/manager.rs +++ b/core/src/job/manager.rs @@ -29,7 +29,7 @@ use uuid::Uuid; use super::{JobManagerError, JobReport, JobStatus, StatefulJob}; // db is single threaded, nerd -const MAX_WORKERS: usize = 1; +const MAX_WORKERS: usize = 5; pub enum JobManagerEvent { IngestJob(Arc, Box), diff --git a/core/src/object/fs/copy.rs b/core/src/object/fs/copy.rs index ba6851b75..1d5df86b9 100644 --- a/core/src/object/fs/copy.rs +++ b/core/src/object/fs/copy.rs @@ -13,7 +13,7 @@ use crate::{ }, }; -use std::{ffi::OsStr, hash::Hash, path::PathBuf}; +use std::{hash::Hash, path::PathBuf}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -22,8 +22,8 @@ use tokio::{fs, io}; use tracing::{trace, warn}; use super::{ - append_digit_to_filename, construct_target_filename, error::FileSystemJobsError, - fetch_source_and_target_location_paths, get_file_data_from_isolated_file_path, + construct_target_filename, error::FileSystemJobsError, fetch_source_and_target_location_paths, + find_available_filename_for_duplicate, get_file_data_from_isolated_file_path, get_many_files_datas, FileData, }; @@ -173,68 +173,27 @@ impl StatefulJob for FileCopierJobInit { } else { match fs::metadata(target_full_path).await { Ok(_) => { - let new_file_name = - target_full_path - .file_stem() - .ok_or(JobError::JobDataNotFound( - "No stem on file path, but it's supposed to be a file".to_string(), - ))?; - - let new_file_full_path_without_suffix = target_full_path.parent().map_or_else( - || { - Err(JobError::JobDataNotFound( - "No parent for file path, which is supposed to be directory" - .to_string(), - )) - }, - |x| Ok(x.to_path_buf()), - )?; - - for i in 1..u32::MAX { - let mut new_file_full_path_candidate = - new_file_full_path_without_suffix.clone(); - - append_digit_to_filename( - &mut new_file_full_path_candidate, - new_file_name.to_str().ok_or(JobError::JobDataNotFound( - "Unable to convert file name to &str".to_string(), - ))?, - target_full_path.extension().and_then(OsStr::to_str), - i, - ); - - match fs::metadata(&new_file_full_path_candidate).await { - Ok(_) => { - // This candidate already exists, so we try the next one - continue; - } - Err(e) if e.kind() == io::ErrorKind::NotFound => { - fs::copy( - &source_file_data.full_path, - &new_file_full_path_candidate, - ) + // Already exist a file with this name, so we need to find an available name + match find_available_filename_for_duplicate(target_full_path).await { + Ok(new_path) => { + fs::copy(&source_file_data.full_path, &new_path) .await // Using the ? here because we don't want to increase the completed task // count in case of file system errors - .map_err(|e| { - FileIOError::from((new_file_full_path_candidate, e)) - })?; + .map_err(|e| FileIOError::from((new_path, e)))?; - break; - } - Err(e) => { - return Err( - FileIOError::from((new_file_full_path_candidate, e)).into() - ) - } + Ok(().into()) } - } - Ok(JobRunErrors(vec![FileSystemJobsError::WouldOverwrite( - target_full_path.clone().into_boxed_path(), - ) - .to_string()]) - .into()) + Err(FileSystemJobsError::FailedToFindAvailableName(path)) => { + Ok(JobRunErrors(vec![ + FileSystemJobsError::WouldOverwrite(path).to_string() + ]) + .into()) + } + + Err(e) => Err(e.into()), + } } Err(e) if e.kind() == io::ErrorKind::NotFound => { trace!( @@ -251,7 +210,7 @@ impl StatefulJob for FileCopierJobInit { Ok(().into()) } - Err(e) => return Err(FileIOError::from((target_full_path, e)).into()), + Err(e) => Err(FileIOError::from((target_full_path, e)).into()), } } } diff --git a/core/src/object/fs/create.rs b/core/src/object/fs/create.rs deleted file mode 100644 index 9f9857cf9..000000000 --- a/core/src/object/fs/create.rs +++ /dev/null @@ -1,30 +0,0 @@ -// use crate::{ -// library::LibraryContext, -// location::{check_virtual_path_exists, fetch_location, LocationError}, -// }; - -// use super::error::VirtualFSError; -// use crate::prisma::{file_path, location}; - -// // TODO: we should create an action handler for all FS operations, that can work for both local and remote locations -// // if the location is remote, we queue a job for that client specifically -// // the actual create_folder function should be an option on an enum for all vfs actions -// pub async fn create_folder( -// location_id: location::id::Type, -// path: &str, -// name: Option<&str>, -// library: &LibraryContext, -// ) -> Result<(), VirtualFSError> { -// let location = fetch_location(library, location_id) -// .exec() -// .await? -// .ok_or(LocationError::IdNotFound(location_id))?; - -// let name = name.unwrap_or("Untitled Folder"); - -// let exists = check_virtual_path_exists(library, location_id, subpath).await?; - -// std::fs::create_dir_all(&obj_path)?; - -// Ok(()) -// } diff --git a/core/src/object/fs/error.rs b/core/src/object/fs/error.rs index 6ce5ae6d2..c39975b34 100644 --- a/core/src/object/fs/error.rs +++ b/core/src/object/fs/error.rs @@ -1,7 +1,10 @@ use crate::{ location::{file_path_helper::FilePathError, LocationError}, prisma::file_path, - util::{db::MissingFieldError, error::FileIOError}, + util::{ + db::MissingFieldError, + error::{FileIOError, NonUtf8PathError}, + }, }; use std::path::Path; @@ -28,4 +31,20 @@ pub enum FileSystemJobsError { WouldOverwrite(Box), #[error("missing-field: {0}")] MissingField(#[from] MissingFieldError), + #[error("no parent for path, which is supposed to be directory: ", .0.display())] + MissingParentPath(Box), + #[error("no stem on file path, but it's supposed to be a file: ", .0.display())] + MissingFileStem(Box), + #[error(transparent)] + FileIO(#[from] FileIOError), + #[error(transparent)] + NonUTF8Path(#[from] NonUtf8PathError), + #[error("failed to find an available name to avoid duplication: ", .0.display())] + FailedToFindAvailableName(Box), +} + +impl From for rspc::Error { + fn from(e: FileSystemJobsError) -> Self { + Self::with_cause(rspc::ErrorCode::InternalServerError, e.to_string(), e) + } } diff --git a/core/src/object/fs/mod.rs b/core/src/object/fs/mod.rs index 6f337480d..8bc8b865a 100644 --- a/core/src/object/fs/mod.rs +++ b/core/src/object/fs/mod.rs @@ -4,16 +4,21 @@ use crate::{ LocationError, }, prisma::{file_path, location, PrismaClient}, - util::db::{maybe_missing, MissingFieldError}, + util::{ + db::{maybe_missing, MissingFieldError}, + error::{FileIOError, NonUtf8PathError}, + }, }; -use std::path::{Path, PathBuf}; +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; use once_cell::sync::Lazy; use regex::Regex; use serde::{Deserialize, Serialize}; -pub mod create; pub mod delete; pub mod erase; @@ -26,6 +31,7 @@ pub mod cut; pub mod error; use error::FileSystemJobsError; +use tokio::{fs, io}; static DUPLICATE_PATTERN: Lazy = Lazy::new(|| Regex::new(r" \(\d+\)").expect("Failed to compile hardcoded regex")); @@ -167,3 +173,48 @@ pub fn append_digit_to_filename( final_path.push(format!("{new_file_name} ({current_int})")); } } + +pub async fn find_available_filename_for_duplicate( + target_path: impl AsRef, +) -> Result { + let target_path = target_path.as_ref(); + + let new_file_name = target_path + .file_stem() + .ok_or_else(|| { + FileSystemJobsError::MissingFileStem(target_path.to_path_buf().into_boxed_path()) + })? + .to_str() + .ok_or_else(|| NonUtf8PathError(target_path.to_path_buf().into_boxed_path()))?; + + let new_file_full_path_without_suffix = + target_path.parent().map(Path::to_path_buf).ok_or_else(|| { + FileSystemJobsError::MissingParentPath(target_path.to_path_buf().into_boxed_path()) + })?; + + for i in 1..u32::MAX { + let mut new_file_full_path_candidate = new_file_full_path_without_suffix.clone(); + + append_digit_to_filename( + &mut new_file_full_path_candidate, + new_file_name, + target_path.extension().and_then(OsStr::to_str), + i, + ); + + match fs::metadata(&new_file_full_path_candidate).await { + Ok(_) => { + // This candidate already exists, so we try the next one + continue; + } + Err(e) if e.kind() == io::ErrorKind::NotFound => { + return Ok(new_file_full_path_candidate); + } + Err(e) => return Err(FileIOError::from((new_file_full_path_candidate, e)).into()), + } + } + + Err(FileSystemJobsError::FailedToFindAvailableName( + target_path.to_path_buf().into_boxed_path(), + )) +} diff --git a/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx index d5d48f872..7a3681cff 100644 --- a/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx +++ b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx @@ -3,6 +3,7 @@ import { libraryClient, useLibraryMutation } from '@sd/client'; import { ContextMenu, dialogManager, ModifierKeys, toast } from '@sd/ui'; import { Menu } from '~/components/Menu'; import { useKeybindFactory } from '~/hooks/useKeybindFactory'; +import { useQuickRescan } from '~/hooks/useQuickRescan'; import { isNonEmpty } from '~/util'; import { useExplorerContext } from '../../Context'; @@ -27,6 +28,8 @@ export const Delete = new ConditionalItem({ Component: ({ selectedFilePaths, locationId }) => { const keybind = useKeybindFactory(); + const rescan = useQuickRescan(); + return ( ( p.id)} /> diff --git a/interface/app/$libraryId/Explorer/FilePath/DeleteDialog.tsx b/interface/app/$libraryId/Explorer/FilePath/DeleteDialog.tsx index 21a68cefc..52307caf5 100644 --- a/interface/app/$libraryId/Explorer/FilePath/DeleteDialog.tsx +++ b/interface/app/$libraryId/Explorer/FilePath/DeleteDialog.tsx @@ -3,6 +3,7 @@ import { CheckBox, Dialog, Tooltip, useDialog, UseDialogProps } from '@sd/ui'; interface Props extends UseDialogProps { locationId: number; + rescan?: () => void; pathIds: number[]; } @@ -14,12 +15,14 @@ export default (props: Props) => { return ( - deleteFile.mutateAsync({ + onSubmit={form.handleSubmit(async () => { + await deleteFile.mutateAsync({ location_id: props.locationId, file_path_ids: props.pathIds - }) - )} + }); + + props.rescan?.(); + })} dialog={useDialog(props)} title="Delete a file" description="Warning: This will delete your file forever, we don't have a trash can yet..." diff --git a/interface/app/$libraryId/Explorer/TopBarOptions.tsx b/interface/app/$libraryId/Explorer/TopBarOptions.tsx index 9f1528f82..fd52fae0f 100644 --- a/interface/app/$libraryId/Explorer/TopBarOptions.tsx +++ b/interface/app/$libraryId/Explorer/TopBarOptions.tsx @@ -1,5 +1,6 @@ import { ArrowClockwise, + FolderPlus, Key, MonitorPlay, Rows, @@ -8,11 +9,14 @@ import { SquaresFour, Tag } from '@phosphor-icons/react'; +import { useQueryClient } from '@tanstack/react-query'; import clsx from 'clsx'; import { useEffect, useRef } from 'react'; -import { useRspcLibraryContext } from '@sd/client'; -import { useKeyMatcher } from '~/hooks'; +import { useLibraryMutation, useLibraryQuery, useRspcLibraryContext } from '@sd/client'; +import { ModifierKeys, toast } from '@sd/ui'; +import { useKeybind, useKeyMatcher, useOperatingSystem } from '~/hooks'; +import { useQuickRescan } from '../../../hooks/useQuickRescan'; import { KeyManager } from '../KeyManager'; import TopBarOptions, { ToolOption, TOP_BAR_ICON_STYLE } from '../TopBar/TopBarOptions'; import { useExplorerContext } from './Context'; @@ -24,9 +28,21 @@ export const useExplorerTopBarOptions = () => { const explorerStore = useExplorerStore(); const explorer = useExplorerContext(); const controlIcon = useKeyMatcher('Meta').icon; - const settings = explorer.useSettingsSnapshot(); + const rescan = useQuickRescan(); + + const createFolder = useLibraryMutation(['files.createFolder'], { + onError: (e) => { + toast.error({ title: 'Error creating folder', body: `Error: ${e}.` }); + console.error(e); + }, + onSuccess: (folder) => { + toast.success({ title: `Created new folder "${folder}"` }); + rescan(); + } + }); + const viewOptions: ToolOption[] = [ { toolTipLabel: 'Grid view', @@ -67,7 +83,7 @@ export const useExplorerTopBarOptions = () => { icon: , popOverComponent: , individual: true, - showAtResolution: 'xl:flex' + showAtResolution: 'sm:flex' }, { toolTipLabel: 'Show Inspector', @@ -87,19 +103,28 @@ export const useExplorerTopBarOptions = () => { } ]; - // subscription so that we can cancel it if in progress - const quickRescanSubscription = useRef<() => void | undefined>(); - - // gotta clean up any rescan subscriptions if the exist - useEffect(() => () => quickRescanSubscription.current?.(), []); - - const { client } = useRspcLibraryContext(); - const { parent } = useExplorerContext(); const [{ path }] = useExplorerSearchParams(); + const os = useOperatingSystem(); + + useKeybind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'r'], () => rescan()); + const toolOptions = [ + parent?.type === 'Location' && { + toolTipLabel: 'New Folder', + icon: , + onClick: () => { + createFolder.mutate({ + location_id: parent.location.id, + sub_path: path || null, + name: null + }); + }, + individual: true, + showAtResolution: 'xs:flex' + }, { toolTipLabel: 'Key Manager', icon: , @@ -122,19 +147,7 @@ export const useExplorerTopBarOptions = () => { }, parent?.type === 'Location' && { toolTipLabel: 'Reload', - onClick: () => { - quickRescanSubscription.current?.(); - quickRescanSubscription.current = client.addSubscription( - [ - 'locations.quickRescan', - { - location_id: parent.location.id, - sub_path: path ?? '' - } - ], - { onData() {} } - ); - }, + onClick: rescan, icon: , individual: true, showAtResolution: 'xl:flex' diff --git a/interface/app/$libraryId/Layout/Sidebar/EphemeralSection.tsx b/interface/app/$libraryId/Layout/Sidebar/EphemeralSection.tsx index 213c10974..d503a2aa8 100644 --- a/interface/app/$libraryId/Layout/Sidebar/EphemeralSection.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/EphemeralSection.tsx @@ -53,8 +53,6 @@ export const EphemeralSection = () => { return locationIdsMap; }, [locations.data, volumes.data]); - console.log('locationIdsForVolumes', locationIdsForVolumes); - const items = [ { type: 'network' }, home ? { type: 'home', path: home } : null, diff --git a/interface/app/$libraryId/Layout/Sidebar/LibrarySection.tsx b/interface/app/$libraryId/Layout/Sidebar/LibrarySection.tsx index a5ce4eb03..e81945596 100644 --- a/interface/app/$libraryId/Layout/Sidebar/LibrarySection.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/LibrarySection.tsx @@ -60,8 +60,6 @@ export const LibrarySection = () => { null ); - const [seeMoreLocations, setSeeMoreLocations] = useState(false); - useEffect(() => { const outsideClick = () => { document.addEventListener('click', () => { diff --git a/interface/app/$libraryId/location/$id.tsx b/interface/app/$libraryId/location/$id.tsx index da1d9f688..30c77bcfa 100644 --- a/interface/app/$libraryId/location/$id.tsx +++ b/interface/app/$libraryId/location/$id.tsx @@ -1,7 +1,11 @@ +import { Info } from '@phosphor-icons/react'; +import { getIcon, iconNames } from '@sd/assets/util'; +import clsx from 'clsx'; import { useCallback, useEffect, useMemo } from 'react'; import { useDebouncedCallback } from 'use-debounce'; import { stringify } from 'uuid'; import { + arraysEqual, ExplorerSettings, FilePathFilterArgs, FilePathOrder, @@ -10,19 +14,23 @@ import { useLibraryMutation, useLibraryQuery, useLibrarySubscription, + useOnlineLocations, useRspcLibraryContext } from '@sd/client'; +import { Tooltip } from '@sd/ui'; import { LocationIdParamsSchema } from '~/app/route-schemas'; import { Folder } from '~/components'; import { useKeyDeleteFile, useZodRouteParams } from '~/hooks'; import Explorer from '../Explorer'; import { ExplorerContextProvider } from '../Explorer/Context'; +import { InfoPill } from '../Explorer/Inspector'; import { usePathsInfiniteQuery } from '../Explorer/queries'; import { createDefaultExplorerSettings, filePathOrderingKeysSchema } from '../Explorer/store'; import { DefaultTopBarOptions } from '../Explorer/TopBarOptions'; import { useExplorer, UseExplorerSettings, useExplorerSettings } from '../Explorer/useExplorer'; import { useExplorerSearchParams } from '../Explorer/util'; +import { EmptyNotice } from '../Explorer/View'; import { TopBarPortal } from '../TopBar/Portal'; import LocationOptions from './LocationOptions'; @@ -32,6 +40,14 @@ export const Component = () => { const location = useLibraryQuery(['locations.get', locationId]); const rspc = useRspcLibraryContext(); + const onlineLocations = useOnlineLocations(); + + const locationOnline = useMemo(() => { + const pub_id = location.data?.pub_id; + if (!pub_id) return false; + return onlineLocations.some((l) => arraysEqual(pub_id, l)); + }, [location.data?.pub_id, onlineLocations]); + const preferences = useLibraryQuery(['preferences.get']); const updatePreferences = useLibraryMutation('preferences.update'); @@ -117,6 +133,11 @@ export const Component = () => { ? getLastSectionOfPath(path) : location.data?.name} + {!locationOnline && ( + + + + )} {location.data && ( )} @@ -125,7 +146,15 @@ export const Component = () => { right={} /> - + } + message="No files found here" + /> + } + /> ); }; diff --git a/interface/hooks/useQuickRescan.ts b/interface/hooks/useQuickRescan.ts new file mode 100644 index 000000000..2ba155ade --- /dev/null +++ b/interface/hooks/useQuickRescan.ts @@ -0,0 +1,38 @@ +import { useEffect, useRef } from 'react'; +import { useRspcLibraryContext } from '@sd/client'; + +import { useExplorerContext } from '../app/$libraryId/Explorer/Context'; +import { useExplorerSearchParams } from '../app/$libraryId/Explorer/util'; +import { useOperatingSystem } from './useOperatingSystem'; + +export const useQuickRescan = () => { + // subscription so that we can cancel it if in progress + const quickRescanSubscription = useRef<() => void | undefined>(); + + // gotta clean up any rescan subscriptions if the exist + useEffect(() => () => quickRescanSubscription.current?.(), []); + + const { client } = useRspcLibraryContext(); + + const { parent } = useExplorerContext(); + + const [{ path }] = useExplorerSearchParams(); + + const rescan = () => { + if (parent?.type === 'Location') { + quickRescanSubscription.current?.(); + quickRescanSubscription.current = client.addSubscription( + [ + 'locations.quickRescan', + { + location_id: parent.location.id, + sub_path: path ?? '' + } + ], + { onData() {} } + ); + } + }; + + return rescan; +}; diff --git a/packages/assets/icons/Drive-AmazonS3.png b/packages/assets/icons/Drive-AmazonS3.png new file mode 100644 index 000000000..a734124a8 Binary files /dev/null and b/packages/assets/icons/Drive-AmazonS3.png differ diff --git a/packages/assets/icons/Drive-BackBlaze.png b/packages/assets/icons/Drive-BackBlaze.png new file mode 100644 index 000000000..29ba78c25 Binary files /dev/null and b/packages/assets/icons/Drive-BackBlaze.png differ diff --git a/packages/assets/icons/Drive-Box.png b/packages/assets/icons/Drive-Box.png new file mode 100644 index 000000000..9f7f14ca6 Binary files /dev/null and b/packages/assets/icons/Drive-Box.png differ diff --git a/packages/assets/icons/Drive-DAV.png b/packages/assets/icons/Drive-DAV.png new file mode 100644 index 000000000..bd406f322 Binary files /dev/null and b/packages/assets/icons/Drive-DAV.png differ diff --git a/packages/assets/icons/Drive-Dropbox.png b/packages/assets/icons/Drive-Dropbox.png new file mode 100644 index 000000000..5eaeca287 Binary files /dev/null and b/packages/assets/icons/Drive-Dropbox.png differ diff --git a/packages/assets/icons/Drive-GoogleDrive.png b/packages/assets/icons/Drive-GoogleDrive.png new file mode 100644 index 000000000..d6153c201 Binary files /dev/null and b/packages/assets/icons/Drive-GoogleDrive.png differ diff --git a/packages/assets/icons/Drive-Mega.png b/packages/assets/icons/Drive-Mega.png new file mode 100644 index 000000000..74687cf4f Binary files /dev/null and b/packages/assets/icons/Drive-Mega.png differ diff --git a/packages/assets/icons/Drive-OneDrive.png b/packages/assets/icons/Drive-OneDrive.png new file mode 100644 index 000000000..96d5796ea Binary files /dev/null and b/packages/assets/icons/Drive-OneDrive.png differ diff --git a/packages/assets/icons/Drive-OpenStack.png b/packages/assets/icons/Drive-OpenStack.png new file mode 100644 index 000000000..42f088515 Binary files /dev/null and b/packages/assets/icons/Drive-OpenStack.png differ diff --git a/packages/assets/icons/Drive-PCloud.png b/packages/assets/icons/Drive-PCloud.png new file mode 100644 index 000000000..e17c1d805 Binary files /dev/null and b/packages/assets/icons/Drive-PCloud.png differ diff --git a/packages/assets/icons/FolderNoSpace.png b/packages/assets/icons/FolderNoSpace.png new file mode 100644 index 000000000..ac1dfad0b Binary files /dev/null and b/packages/assets/icons/FolderNoSpace.png differ diff --git a/packages/assets/icons/HDD.png b/packages/assets/icons/HDD.png index d50d9dc80..58eeea241 100644 Binary files a/packages/assets/icons/HDD.png and b/packages/assets/icons/HDD.png differ diff --git a/packages/assets/icons/Home.png b/packages/assets/icons/Home.png index 05af1722f..f019c6200 100644 Binary files a/packages/assets/icons/Home.png and b/packages/assets/icons/Home.png differ diff --git a/packages/assets/icons/Spacedrop-1.png b/packages/assets/icons/Spacedrop-1.png new file mode 100644 index 000000000..bd280f642 Binary files /dev/null and b/packages/assets/icons/Spacedrop-1.png differ diff --git a/packages/assets/icons/Spacedrop.png b/packages/assets/icons/Spacedrop.png index aa9f1a7ac..7610935d2 100644 Binary files a/packages/assets/icons/Spacedrop.png and b/packages/assets/icons/Spacedrop.png differ diff --git a/packages/assets/icons/index.ts b/packages/assets/icons/index.ts index 62771cf40..caff44ca3 100644 --- a/packages/assets/icons/index.ts +++ b/packages/assets/icons/index.ts @@ -34,6 +34,16 @@ import Document_xls_Light from './Document_xls_Light.png'; import Document_xls from './Document_xls.png'; import Document from './Document.png'; import Drive_Light from './Drive_Light.png'; +import DriveAmazonS3 from './Drive-AmazonS3.png'; +import DriveBackBlaze from './Drive-BackBlaze.png'; +import DriveBox from './Drive-Box.png'; +import DriveDAV from './Drive-DAV.png'; +import DriveDropbox from './Drive-Dropbox.png'; +import DriveGoogleDrive from './Drive-GoogleDrive.png'; +import DriveMega from './Drive-Mega.png'; +import DriveOneDrive from './Drive-OneDrive.png'; +import DriveOpenStack from './Drive-OpenStack.png'; +import DrivePCloud from './Drive-PCloud.png'; import Drive from './Drive.png'; import Dropbox from './Dropbox.png'; import Encrypted_Light from './Encrypted_Light.png'; @@ -49,6 +59,7 @@ import Folder_Light from './Folder_Light.png'; import Folder from './Folder.png'; import FolderGrey_Light from './FolderGrey_Light.png'; import FolderGrey from './FolderGrey.png'; +import FolderNoSpace from './FolderNoSpace.png'; import Game_Light from './Game_Light.png'; import Game from './Game.png'; import Globe from './Globe.png'; @@ -91,6 +102,7 @@ import ScreenshotAlt from './ScreenshotAlt.png'; import SD from './SD.png'; import Server_Light from './Server_Light.png'; import Server from './Server.png'; +import Spacedrop1 from './Spacedrop-1.png'; import Spacedrop from './Spacedrop.png'; import Tablet_Light from './Tablet_Light.png'; import Tablet from './Tablet.png'; @@ -145,6 +157,16 @@ export { Document_pdf_Light, Document_xls, Document_xls_Light, + DriveAmazonS3, + DriveBackBlaze, + DriveBox, + DriveDAV, + DriveDropbox, + DriveGoogleDrive, + DriveMega, + DriveOneDrive, + DriveOpenStack, + DrivePCloud, Drive, Drive_Light, Dropbox, @@ -160,6 +182,7 @@ export { Folder, FolderGrey, FolderGrey_Light, + FolderNoSpace, Folder_Light, Game, Game_Light, @@ -203,6 +226,7 @@ export { ScreenshotAlt, Server, Server_Light, + Spacedrop1, Spacedrop, Tablet, Tablet_Light, diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 831e16f87..b4fd5475f 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -48,6 +48,8 @@ export type Procedures = { { key: "backups.restore", input: string, result: null } | { key: "files.convertImage", input: LibraryArgs, result: null } | { key: "files.copyFiles", input: LibraryArgs, result: null } | + { key: "files.createEphemeralFolder", input: LibraryArgs, result: string } | + { key: "files.createFolder", input: LibraryArgs, result: string } | { key: "files.cutFiles", input: LibraryArgs, result: null } | { key: "files.deleteFiles", input: LibraryArgs, result: null } | { key: "files.duplicateFiles", input: LibraryArgs, result: null } | @@ -136,6 +138,10 @@ export type ConvertImageArgs = { location_id: number; file_path_id: number; dele export type ConvertableExtension = "bmp" | "dib" | "ff" | "gif" | "ico" | "jpg" | "jpeg" | "png" | "pnm" | "qoi" | "tga" | "icb" | "vda" | "vst" | "tiff" | "tif" | "heif" | "heifs" | "heic" | "heics" | "avif" | "avci" | "avcs" | "svg" | "svgz" | "pdf" +export type CreateEphemeralFolderArgs = { path: string; name: string | null } + +export type CreateFolderArgs = { location_id: number; sub_path: string | null; name: string | null } + export type CreateLibraryArgs = { name: LibraryName } export type CursorOrderItem = { order: SortOrder; data: T }