mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-04 13:26:00 -04:00
[ENG-779] Finalize UI (#986)
* [ENG-779] Finalize UI This is one branch with a variety of UI changes add tag select mode bar without functionality fix group job status add notice icon with info to stat icons add WIP notice to media view add modal before add location with greyed out clouds remove disappearing add location button add WIP spacedrop page bring back limited key manager UI add options bar on search page without functionality Add greyed out encrypt library button or setting See more button on locations Show locations on node screen Fix overview category left padding * key manager placeholder * stat info * nodes screen * location click yay * fix size in bytes Co-authored-by: Brendan Allan <Brendonovich@users.noreply.github.com> * small ui improvements * sh*tty see more button * last touches * fix merge boo boo * Fix mobile - Move `getItemObject`, `getItemFilePath`, `getItemLocation`, `getExplorerItemData` to @sd/core to allow mobile to use them * Formatting * Normalize displayed file size between all screens - Replace every use of internal formatBytes with byte-size dep --------- Co-authored-by: Brendan Allan <Brendonovich@users.noreply.github.com> Co-authored-by: Vítor Vasconcellos <vasconcellos.dev@gmail.com>
This commit is contained in:
@@ -41,6 +41,7 @@ poonen
|
||||
rauch
|
||||
ravikant
|
||||
Recents
|
||||
Renamable
|
||||
richelsen
|
||||
rspc
|
||||
rspcws
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Text, View } from 'react-native';
|
||||
import { ExplorerItem, isObject } from '@sd/client';
|
||||
import { ExplorerItem, getItemFilePath } from '@sd/client';
|
||||
import Layout from '~/constants/Layout';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { getExplorerStore } from '~/stores/explorerStore';
|
||||
@@ -12,7 +12,7 @@ type FileItemProps = {
|
||||
const FileItem = ({ data }: FileItemProps) => {
|
||||
const gridItemSize = Layout.window.width / getExplorerStore().gridNumColumns;
|
||||
|
||||
const filePath = isObject(data) ? data.item.file_paths[0] : data.item;
|
||||
const filePath = getItemFilePath(data);
|
||||
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { ExplorerItem, isObject } from '@sd/client';
|
||||
import { ExplorerItem, getItemFilePath } from '@sd/client';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { getExplorerStore } from '~/stores/explorerStore';
|
||||
import FileThumb from './FileThumb';
|
||||
@@ -10,7 +10,7 @@ type FileRowProps = {
|
||||
};
|
||||
|
||||
const FileRow = ({ data }: FileRowProps) => {
|
||||
const filePath = isObject(data) ? data.item.file_paths[0] : data.item;
|
||||
const filePath = getItemFilePath(data);
|
||||
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as icons from '@sd/assets/icons';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Image, View } from 'react-native';
|
||||
import { DocumentDirectoryPath } from 'react-native-fs';
|
||||
import { ExplorerItem, ObjectKind, isObject, isPath } from '@sd/client';
|
||||
import { ExplorerItem, ObjectKind, getItemFilePath, getItemObject, isPath } from '@sd/client';
|
||||
import { tw } from '../../lib/tailwind';
|
||||
import FolderIcon from '../icons/FolderIcon';
|
||||
|
||||
@@ -23,9 +23,8 @@ export const getThumbnailUrlById = (keyParts: string[]) =>
|
||||
type KindType = keyof typeof icons | 'Unknown';
|
||||
|
||||
function getExplorerItemData(data: ExplorerItem) {
|
||||
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
|
||||
|
||||
const filePath = isObject(data) ? data.item.file_paths[0] : data.item;
|
||||
const objectData = getItemObject(data);
|
||||
const filePath = getItemFilePath(data);
|
||||
|
||||
return {
|
||||
casId: filePath?.cas_id || null,
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Alert, Pressable, View, ViewStyle } from 'react-native';
|
||||
import { ExplorerItem, ObjectKind, isObject, isPath, useLibraryQuery } from '@sd/client';
|
||||
import {
|
||||
ExplorerItem,
|
||||
ObjectKind,
|
||||
getItemFilePath,
|
||||
getItemObject,
|
||||
isPath,
|
||||
useLibraryQuery
|
||||
} from '@sd/client';
|
||||
import { InfoPill, PlaceholderPill } from '~/components/primitive/InfoPill';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
||||
@@ -10,8 +17,8 @@ type Props = {
|
||||
};
|
||||
|
||||
const InfoTagPills = ({ data, style }: Props) => {
|
||||
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
|
||||
const filePath = isObject(data) ? data.item.file_paths[0] : data.item;
|
||||
const objectData = getItemObject(data);
|
||||
const filePath = getItemFilePath(data);
|
||||
|
||||
const tagsQuery = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1], {
|
||||
enabled: Boolean(objectData)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import byteSize from 'byte-size';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Copy,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
} from 'phosphor-react-native';
|
||||
import { PropsWithChildren, useRef } from 'react';
|
||||
import { Pressable, Text, View, ViewStyle } from 'react-native';
|
||||
import { formatBytes, isObject } from '@sd/client';
|
||||
import { bytesToNumber, getItemFilePath, getItemObject } from '@sd/client';
|
||||
import FileThumb from '~/components/explorer/FileThumb';
|
||||
import FavoriteButton from '~/components/explorer/sections/FavoriteButton';
|
||||
import InfoTagPills from '~/components/explorer/sections/InfoTagPills';
|
||||
@@ -60,8 +61,8 @@ export const ActionsModal = () => {
|
||||
|
||||
const { modalRef, data } = useActionsModalStore();
|
||||
|
||||
const filePath = data ? (isObject(data) ? data.item.file_paths[0] : data.item) : null;
|
||||
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
|
||||
const objectData = data && getItemObject(data);
|
||||
const filePath = data && getItemFilePath(data);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -84,7 +85,12 @@ export const ActionsModal = () => {
|
||||
</Text>
|
||||
<View style={tw`flex flex-row`}>
|
||||
<Text style={tw`text-xs text-ink-faint`}>
|
||||
{formatBytes(Number(filePath?.size_in_bytes || 0))},
|
||||
{filePath?.size_in_bytes_bytes
|
||||
? byteSize(
|
||||
bytesToNumber(filePath.size_in_bytes_bytes)
|
||||
).toString()
|
||||
: 0}
|
||||
,
|
||||
</Text>
|
||||
<Text style={tw`text-xs text-ink-faint`}>
|
||||
{' '}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import byteSize from 'byte-size';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Barcode,
|
||||
@@ -10,7 +11,13 @@ import {
|
||||
} from 'phosphor-react-native';
|
||||
import { forwardRef } from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { ExplorerItem, formatBytes, isObject, useLibraryQuery } from '@sd/client';
|
||||
import {
|
||||
ExplorerItem,
|
||||
bytesToNumber,
|
||||
getItemFilePath,
|
||||
getItemObject,
|
||||
useLibraryQuery
|
||||
} from '@sd/client';
|
||||
import FileThumb from '~/components/explorer/FileThumb';
|
||||
import InfoTagPills from '~/components/explorer/sections/InfoTagPills';
|
||||
import { Modal, ModalRef, ModalScrollView } from '~/components/layout/Modal';
|
||||
@@ -52,8 +59,8 @@ const FileInfoModal = forwardRef<ModalRef, FileInfoModalProps>((props, ref) => {
|
||||
|
||||
const item = data?.item;
|
||||
|
||||
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
|
||||
const filePathData = data ? (isObject(data) ? data.item.file_paths[0] : data.item) : null;
|
||||
const objectData = data && getItemObject(data);
|
||||
const filePathData = data && getItemFilePath(data);
|
||||
|
||||
const fullObjectData = useLibraryQuery(['files.get', { id: objectData?.id || -1 }], {
|
||||
enabled: objectData?.id !== undefined
|
||||
@@ -90,7 +97,13 @@ const FileInfoModal = forwardRef<ModalRef, FileInfoModalProps>((props, ref) => {
|
||||
<MetaItem
|
||||
title="Size"
|
||||
icon={Cube}
|
||||
value={formatBytes(Number(filePathData?.size_in_bytes || 0))}
|
||||
value={
|
||||
filePathData?.size_in_bytes_bytes
|
||||
? byteSize(
|
||||
bytesToNumber(filePathData.size_in_bytes_bytes)
|
||||
).toString()
|
||||
: 0
|
||||
}
|
||||
/>
|
||||
{/* Duration */}
|
||||
{fullObjectData.data?.media_data?.duration_seconds && (
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "file_path" ADD COLUMN "size_in_bytes_bytes" BLOB;
|
||||
@@ -129,7 +129,8 @@ model FilePath {
|
||||
name String?
|
||||
extension String?
|
||||
|
||||
size_in_bytes String?
|
||||
size_in_bytes String? // deprecated
|
||||
size_in_bytes_bytes Bytes?
|
||||
|
||||
inode Bytes? // This is actually an unsigned 64 bit integer, but we don't have this type in SQLite
|
||||
device Bytes? // This is actually an unsigned 64 bit integer, but we don't have this type in SQLite
|
||||
|
||||
@@ -17,7 +17,7 @@ use specta::Type;
|
||||
|
||||
use super::{utils::library, Ctx, R};
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Debug)]
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ExplorerContext {
|
||||
Location(location::Data),
|
||||
@@ -25,7 +25,7 @@ pub enum ExplorerContext {
|
||||
// Space(object_in_space::Data),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Debug)]
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ExplorerItem {
|
||||
Path {
|
||||
@@ -41,9 +41,14 @@ pub enum ExplorerItem {
|
||||
thumbnail_key: Option<Vec<String>>,
|
||||
item: object_with_file_paths::Data,
|
||||
},
|
||||
Location {
|
||||
has_local_thumbnail: bool,
|
||||
thumbnail_key: Option<Vec<String>>,
|
||||
item: location::Data,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Debug)]
|
||||
#[derive(Serialize, Type, Debug)]
|
||||
pub struct ExplorerData {
|
||||
pub context: ExplorerContext,
|
||||
pub items: Vec<ExplorerItem>,
|
||||
@@ -103,7 +108,9 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
.procedure("update", {
|
||||
R.with2(library())
|
||||
.mutation(|(_, library), args: LocationUpdateArgs| async move {
|
||||
args.update(&library).await.map_err(Into::into)
|
||||
let ret = args.update(&library).await.map_err(Into::into);
|
||||
invalidate_query!(library, "locations.list");
|
||||
ret
|
||||
})
|
||||
})
|
||||
.procedure("delete", {
|
||||
|
||||
@@ -1,34 +1,72 @@
|
||||
use crate::prisma::{location, node};
|
||||
use rspc::{alpha::AlphaRouter, ErrorCode};
|
||||
|
||||
use serde::Deserialize;
|
||||
use specta::Type;
|
||||
use tracing::error;
|
||||
|
||||
use crate::api::R;
|
||||
|
||||
use super::Ctx;
|
||||
use super::{locations::ExplorerItem, utils::library, Ctx, R};
|
||||
|
||||
pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
R.router().procedure("changeNodeName", {
|
||||
#[derive(Deserialize, Type)]
|
||||
pub struct ChangeNodeNameArgs {
|
||||
pub name: String,
|
||||
}
|
||||
// TODO: validate name isn't empty or too long
|
||||
R.router()
|
||||
.procedure("changeNodeName", {
|
||||
#[derive(Deserialize, Type)]
|
||||
pub struct ChangeNodeNameArgs {
|
||||
pub name: String,
|
||||
}
|
||||
// TODO: validate name isn't empty or too long
|
||||
|
||||
R.mutation(|ctx, args: ChangeNodeNameArgs| async move {
|
||||
ctx.config
|
||||
.write(|mut config| {
|
||||
config.name = args.name;
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to write config: {}", err);
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"error updating config".into(),
|
||||
)
|
||||
})
|
||||
.map(|_| ())
|
||||
R.mutation(|ctx, args: ChangeNodeNameArgs| async move {
|
||||
ctx.config
|
||||
.write(|mut config| {
|
||||
config.name = args.name;
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to write config: {}", err);
|
||||
rspc::Error::new(
|
||||
ErrorCode::InternalServerError,
|
||||
"error updating config".into(),
|
||||
)
|
||||
})
|
||||
.map(|_| ())
|
||||
})
|
||||
})
|
||||
// TODO: add pagination!! and maybe ordering etc
|
||||
.procedure("listLocations", {
|
||||
R.with2(library())
|
||||
.query(|(ctx, library), _node_id: Option<String>| async move {
|
||||
// 1. grab currently active node
|
||||
let node_config = ctx.config.get().await;
|
||||
let node_pub_id = node_config.id.as_bytes().to_vec();
|
||||
// 2. get node from database
|
||||
let node = library
|
||||
.db
|
||||
.node()
|
||||
.find_unique(node::pub_id::equals(node_pub_id))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
if let Some(node) = node {
|
||||
// query for locations with that node id
|
||||
let locations: Vec<ExplorerItem> = library
|
||||
.db
|
||||
.location()
|
||||
.find_many(vec![location::node_id::equals(Some(node.id))])
|
||||
.exec()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|location| ExplorerItem::Location {
|
||||
has_local_thumbnail: false,
|
||||
thumbnail_key: None,
|
||||
item: location,
|
||||
})
|
||||
.collect();
|
||||
|
||||
return Ok(locations);
|
||||
}
|
||||
|
||||
Ok(vec![])
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ impl FilePathSearchOrdering {
|
||||
use file_path::*;
|
||||
match self {
|
||||
Self::Name(_) => name::order(dir),
|
||||
Self::SizeInBytes(_) => size_in_bytes::order(dir),
|
||||
Self::SizeInBytes(_) => size_in_bytes_bytes::order(dir),
|
||||
Self::DateCreated(_) => date_created::order(dir),
|
||||
Self::DateModified(_) => date_modified::order(dir),
|
||||
Self::DateIndexed(_) => date_indexed::order(dir),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use prisma_client_rust::not;
|
||||
use sd_p2p::{spacetunnel::Identity, PeerId};
|
||||
use sd_prisma::prisma::node;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -8,7 +9,7 @@ use specta::Type;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
prisma::{indexer_rule, PrismaClient},
|
||||
prisma::{file_path, indexer_rule, PrismaClient},
|
||||
util::{
|
||||
db::uuid_to_bytes,
|
||||
migrator::{Migrate, MigratorError},
|
||||
@@ -61,7 +62,7 @@ impl LibraryConfig {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Migrate for LibraryConfig {
|
||||
const CURRENT_VERSION: u32 = 4;
|
||||
const CURRENT_VERSION: u32 = 5;
|
||||
|
||||
type Ctx = (Uuid, PeerId, Arc<PrismaClient>);
|
||||
|
||||
@@ -135,6 +136,37 @@ impl Migrate for LibraryConfig {
|
||||
config.insert("node_id".into(), Value::String(node_id.to_string()));
|
||||
}
|
||||
4 => {} // -_-
|
||||
5 => loop {
|
||||
let paths = db
|
||||
.file_path()
|
||||
.find_many(vec![not![file_path::size_in_bytes::equals(None)]])
|
||||
.take(500)
|
||||
.select(file_path::select!({ id size_in_bytes }))
|
||||
.exec()
|
||||
.await?;
|
||||
|
||||
if paths.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
db._batch(paths.into_iter().map(|path| {
|
||||
db.file_path().update(
|
||||
file_path::id::equals(path.id),
|
||||
vec![
|
||||
file_path::size_in_bytes_bytes::set(Some(
|
||||
path.size_in_bytes
|
||||
.unwrap()
|
||||
.parse::<u64>()
|
||||
.unwrap()
|
||||
.to_be_bytes()
|
||||
.to_vec(),
|
||||
)),
|
||||
file_path::size_in_bytes::set(None),
|
||||
],
|
||||
)
|
||||
}))
|
||||
.await?;
|
||||
},
|
||||
v => unreachable!("Missing migration for library version {}", v),
|
||||
}
|
||||
|
||||
|
||||
@@ -184,8 +184,8 @@ pub async fn create_file_path(
|
||||
(name::NAME, json!(name)),
|
||||
(extension::NAME, json!(extension)),
|
||||
(
|
||||
size_in_bytes::NAME,
|
||||
json!(metadata.size_in_bytes.to_string()),
|
||||
size_in_bytes_bytes::NAME,
|
||||
json!(metadata.size_in_bytes.to_be_bytes().to_vec()),
|
||||
),
|
||||
(inode::NAME, json!(metadata.inode.to_le_bytes())),
|
||||
(device::NAME, json!(metadata.device.to_le_bytes())),
|
||||
@@ -217,7 +217,7 @@ pub async fn create_file_path(
|
||||
device::set(Some(metadata.device.to_le_bytes().into())),
|
||||
cas_id::set(cas_id),
|
||||
is_dir::set(Some(is_dir)),
|
||||
size_in_bytes::set(Some(metadata.size_in_bytes.to_string())),
|
||||
size_in_bytes_bytes::set(Some(metadata.size_in_bytes.to_be_bytes().to_vec())),
|
||||
date_created::set(Some(metadata.created_at.into())),
|
||||
date_modified::set(Some(metadata.modified_at.into())),
|
||||
]
|
||||
|
||||
@@ -13,6 +13,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
use rspc::ErrorCode;
|
||||
use sd_prisma::prisma_sync;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
@@ -179,10 +180,12 @@ async fn execute_indexer_save_step(
|
||||
),
|
||||
(
|
||||
(
|
||||
size_in_bytes::NAME,
|
||||
json!(entry.metadata.size_in_bytes.to_string()),
|
||||
size_in_bytes_bytes::NAME,
|
||||
json!(entry.metadata.size_in_bytes.to_be_bytes().to_vec()),
|
||||
),
|
||||
size_in_bytes::set(Some(entry.metadata.size_in_bytes.to_string())),
|
||||
size_in_bytes_bytes::set(Some(
|
||||
entry.metadata.size_in_bytes.to_be_bytes().to_vec(),
|
||||
)),
|
||||
),
|
||||
(
|
||||
(inode::NAME, json!(entry.metadata.inode.to_le_bytes())),
|
||||
@@ -200,6 +203,10 @@ async fn execute_indexer_save_step(
|
||||
(date_modified::NAME, json!(entry.metadata.modified_at)),
|
||||
date_modified::set(Some(entry.metadata.modified_at.into())),
|
||||
),
|
||||
(
|
||||
(date_indexed::NAME, json!(Utc::now())),
|
||||
date_indexed::set(Some(Utc::now().into())),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.unzip();
|
||||
|
||||
@@ -376,8 +376,11 @@ async fn inner_update_file(
|
||||
cas_id::set(Some(old_cas_id.clone())),
|
||||
),
|
||||
(
|
||||
(size_in_bytes::NAME, json!(fs_metadata.len().to_string())),
|
||||
size_in_bytes::set(Some(fs_metadata.len().to_string())),
|
||||
(
|
||||
size_in_bytes_bytes::NAME,
|
||||
json!(fs_metadata.len().to_be_bytes().to_vec()),
|
||||
),
|
||||
size_in_bytes_bytes::set(Some(fs_metadata.len().to_be_bytes().to_vec())),
|
||||
),
|
||||
{
|
||||
let date = DateTime::<Local>::from(fs_metadata.modified_or_now()).into();
|
||||
|
||||
@@ -10,14 +10,20 @@ import {
|
||||
Trash,
|
||||
TrashSimple
|
||||
} from 'phosphor-react';
|
||||
import { ExplorerItem, isObject, useLibraryContext, useLibraryMutation } from '@sd/client';
|
||||
import {
|
||||
ExplorerItem,
|
||||
getItemFilePath,
|
||||
getItemObject,
|
||||
useLibraryContext,
|
||||
useLibraryMutation
|
||||
} from '@sd/client';
|
||||
import { ContextMenu, dialogManager } from '@sd/ui';
|
||||
import { getExplorerStore, useExplorerStore, useOperatingSystem } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import AssignTagMenuItems from '../AssignTagMenuItems';
|
||||
import { OpenInNativeExplorer } from '../ContextMenu';
|
||||
import { useExplorerViewContext } from '../ViewContext';
|
||||
import { getItemFilePath, useExplorerSearchParams } from '../util';
|
||||
import { useExplorerSearchParams } from '../util';
|
||||
import OpenWith from './ContextMenu/OpenWith';
|
||||
// import DecryptDialog from './DecryptDialog';
|
||||
import DeleteDialog from './DeleteDialog';
|
||||
@@ -33,7 +39,7 @@ export default ({ data }: Props) => {
|
||||
const explorerView = useExplorerViewContext();
|
||||
const explorerStore = useExplorerStore();
|
||||
const [params] = useExplorerSearchParams();
|
||||
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
|
||||
const objectData = data ? getItemObject(data) : null;
|
||||
|
||||
// const keyManagerUnlocked = useLibraryQuery(['keys.isUnlocked']).data ?? false;
|
||||
// const mountedKeys = useLibraryQuery(['keys.listMounted']);
|
||||
|
||||
@@ -1,66 +1,209 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import clsx from 'clsx';
|
||||
import { HTMLAttributes, useEffect, useRef, useState } from 'react';
|
||||
import { ComponentProps, forwardRef, useEffect, useRef, useState } from 'react';
|
||||
import { useKey } from 'rooks';
|
||||
import { FilePath, useLibraryMutation, useRspcLibraryContext } from '@sd/client';
|
||||
import { useLibraryMutation, useRspcLibraryContext } from '@sd/client';
|
||||
import { showAlertDialog } from '~/components';
|
||||
import useClickOutside from '~/hooks/useClickOutside';
|
||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
import { useExplorerViewContext } from '../ViewContext';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
filePathData: FilePath;
|
||||
type Props = ComponentProps<'div'> & {
|
||||
itemId: number;
|
||||
locationId: number | null;
|
||||
text: string | null;
|
||||
activeClassName?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
renameHandler: (name: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export default ({ filePathData, className, activeClassName, disabled, ...props }: Props) => {
|
||||
const explorerView = useExplorerViewContext();
|
||||
const os = useOperatingSystem();
|
||||
export const RenameTextBoxBase = forwardRef<HTMLDivElement, Props>(
|
||||
({ className, activeClassName, disabled, ...props }, _ref) => {
|
||||
const explorerView = useExplorerViewContext();
|
||||
const os = useOperatingSystem();
|
||||
|
||||
const [allowRename, setAllowRename] = useState(false);
|
||||
const [renamable, setRenamable] = useState(false);
|
||||
|
||||
const funnyRef = useRef<HTMLDivElement>(null);
|
||||
const ref = typeof _ref === 'function' ? { current: funnyRef.current } : _ref;
|
||||
|
||||
// Highlight file name up to extension or
|
||||
// fully if it's a directory or has no extension
|
||||
function highlightText() {
|
||||
if (ref?.current) {
|
||||
const range = document.createRange();
|
||||
const node = ref.current.firstChild;
|
||||
if (!node) return;
|
||||
|
||||
range.setStart(node, 0);
|
||||
range.setEnd(node, props?.text?.length || 0);
|
||||
|
||||
const sel = window.getSelection();
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
}
|
||||
}
|
||||
|
||||
// Blur field
|
||||
function blur() {
|
||||
if (ref?.current) {
|
||||
ref.current.blur();
|
||||
setAllowRename(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset to original file name
|
||||
function reset() {
|
||||
if (ref?.current) {
|
||||
ref.current.innerText = props.text || '';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRename() {
|
||||
if (!ref?.current) return;
|
||||
|
||||
const newName = ref?.current.innerText.trim();
|
||||
if (!newName) return reset();
|
||||
|
||||
if (!props.locationId) return;
|
||||
|
||||
const oldName = props.text;
|
||||
|
||||
if (!oldName || !props.locationId || newName === oldName) return;
|
||||
|
||||
await props.renameHandler(newName);
|
||||
}
|
||||
|
||||
// Handle keydown events
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||
switch (e.key) {
|
||||
case 'Tab':
|
||||
e.preventDefault();
|
||||
blur();
|
||||
break;
|
||||
case 'Escape':
|
||||
reset();
|
||||
blur();
|
||||
break;
|
||||
case 'z':
|
||||
if (os === 'macOS' ? e.metaKey : e.ctrlKey) {
|
||||
reset();
|
||||
highlightText();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Focus and highlight when renaming is allowed
|
||||
useEffect(() => {
|
||||
if (allowRename) {
|
||||
explorerView.setIsRenaming(true);
|
||||
setTimeout(() => {
|
||||
if (ref?.current) {
|
||||
ref.current.focus();
|
||||
highlightText();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [allowRename]);
|
||||
|
||||
// Handle renaming when triggered from outside
|
||||
useEffect(() => {
|
||||
if (!disabled) {
|
||||
if (explorerView.isRenaming && !allowRename) setAllowRename(true);
|
||||
else if (!explorerView.isRenaming && allowRename) setAllowRename(false);
|
||||
}
|
||||
}, [explorerView.isRenaming]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (ref?.current && !ref.current.contains(event.target as Node)) {
|
||||
blur();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside, true);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside, true);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
// Rename or blur on Enter key
|
||||
useKey('Enter', (e) => {
|
||||
if (allowRename) {
|
||||
e.preventDefault();
|
||||
blur();
|
||||
} else if (!disabled) setAllowRename(true);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="textbox"
|
||||
contentEditable={allowRename}
|
||||
suppressContentEditableWarning
|
||||
className={clsx(
|
||||
'cursor-default overflow-y-auto truncate rounded-md px-1.5 py-px text-xs text-ink',
|
||||
allowRename && [
|
||||
'whitespace-normal bg-app outline-none ring-2 ring-accent-deep',
|
||||
activeClassName
|
||||
],
|
||||
className
|
||||
)}
|
||||
onDoubleClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.button === 0 && setRenamable(!disabled)}
|
||||
onMouseUp={(e) => {
|
||||
if (e.button === 0) {
|
||||
if (renamable) {
|
||||
setAllowRename(true);
|
||||
}
|
||||
setRenamable(false);
|
||||
}
|
||||
}}
|
||||
onBlur={async () => {
|
||||
await handleRename();
|
||||
setAllowRename(false);
|
||||
explorerView.setIsRenaming(false);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
>
|
||||
{props.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const RenamePathTextBox = (
|
||||
props: Omit<Props, 'renameHandler'> & { isDir: boolean; extension?: string | null }
|
||||
) => {
|
||||
const rspc = useRspcLibraryContext();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [allowRename, setAllowRename] = useState(false);
|
||||
const [renamable, setRenamable] = useState(false);
|
||||
|
||||
const renameFile = useLibraryMutation(['files.renameFile'], {
|
||||
onError: () => reset(),
|
||||
onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths'])
|
||||
});
|
||||
|
||||
const fileName = `${filePathData?.name}${
|
||||
filePathData?.extension && `.${filePathData.extension}`
|
||||
}`;
|
||||
|
||||
// Reset to original file name
|
||||
function reset() {
|
||||
if (ref.current) {
|
||||
ref.current.innerText = fileName;
|
||||
if (ref?.current) {
|
||||
ref.current.innerText = props.text || '';
|
||||
}
|
||||
}
|
||||
|
||||
const fileName =
|
||||
props.isDir || !props.extension ? props.text : props.text + '.' + props.extension;
|
||||
|
||||
// Handle renaming
|
||||
async function rename() {
|
||||
if (!ref.current) return;
|
||||
|
||||
const newName = ref.current.innerText.trim();
|
||||
if (!newName) return reset();
|
||||
|
||||
if (!filePathData) return;
|
||||
|
||||
const oldName =
|
||||
filePathData.is_dir || !filePathData.extension
|
||||
? filePathData.name
|
||||
: filePathData.name + '.' + filePathData.extension;
|
||||
|
||||
if (!oldName || !filePathData.location_id || newName === oldName) return;
|
||||
|
||||
async function rename(newName: string) {
|
||||
if (!props.locationId || newName === fileName) return;
|
||||
try {
|
||||
await renameFile.mutateAsync({
|
||||
location_id: filePathData.location_id,
|
||||
location_id: props.locationId,
|
||||
kind: {
|
||||
One: {
|
||||
from_file_path_id: filePathData.id,
|
||||
from_file_path_id: props.itemId,
|
||||
to: newName
|
||||
}
|
||||
}
|
||||
@@ -73,126 +216,44 @@ export default ({ filePathData, className, activeClassName, disabled, ...props }
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight file name up to extension or
|
||||
// fully if it's a directory or has no extension
|
||||
function highlightFileName() {
|
||||
if (ref.current) {
|
||||
const range = document.createRange();
|
||||
const node = ref.current.firstChild;
|
||||
if (!node) return;
|
||||
return <RenameTextBoxBase {...props} text={fileName} renameHandler={rename} ref={ref} />;
|
||||
};
|
||||
|
||||
range.setStart(node, 0);
|
||||
range.setEnd(node, filePathData?.name?.length || 0);
|
||||
export const RenameLocationTextBox = (props: Omit<Props, 'renameHandler'>) => {
|
||||
const rspc = useRspcLibraryContext();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const sel = window.getSelection();
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
}
|
||||
}
|
||||
|
||||
// Blur field
|
||||
function blur() {
|
||||
if (ref.current) {
|
||||
ref.current.blur();
|
||||
setAllowRename(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle keydown events
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||
switch (e.key) {
|
||||
case 'Tab':
|
||||
e.preventDefault();
|
||||
blur();
|
||||
break;
|
||||
case 'Escape':
|
||||
reset();
|
||||
blur();
|
||||
break;
|
||||
case 'z':
|
||||
if (os === 'macOS' ? e.metaKey : e.ctrlKey) {
|
||||
reset();
|
||||
highlightFileName();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Focus and highlight when renaming is allowed
|
||||
useEffect(() => {
|
||||
if (allowRename) {
|
||||
explorerView.setIsRenaming(true);
|
||||
setTimeout(() => {
|
||||
if (ref.current) {
|
||||
ref.current.focus();
|
||||
highlightFileName();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [allowRename]);
|
||||
|
||||
// Handle renaming when triggered from outside
|
||||
useEffect(() => {
|
||||
if (!disabled) {
|
||||
if (explorerView.isRenaming && !allowRename) setAllowRename(true);
|
||||
else if (!explorerView.isRenaming && allowRename) setAllowRename(false);
|
||||
}
|
||||
}, [explorerView.isRenaming]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
blur();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside, true);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside, true);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
// Rename or blur on Enter key
|
||||
useKey('Enter', (e) => {
|
||||
if (allowRename) {
|
||||
e.preventDefault();
|
||||
blur();
|
||||
} else if (!disabled) setAllowRename(true);
|
||||
const renameLocation = useLibraryMutation(['locations.update'], {
|
||||
onError: () => reset(),
|
||||
onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths'])
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="textbox"
|
||||
contentEditable={allowRename}
|
||||
suppressContentEditableWarning
|
||||
className={clsx(
|
||||
'cursor-default overflow-y-auto truncate rounded-md px-1.5 py-px text-xs text-ink',
|
||||
allowRename && [
|
||||
'whitespace-normal bg-app outline-none ring-2 ring-accent-deep',
|
||||
activeClassName
|
||||
],
|
||||
className
|
||||
)}
|
||||
onDoubleClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.button === 0 && setRenamable(!disabled)}
|
||||
onMouseUp={(e) => {
|
||||
if (e.button === 0) {
|
||||
if (renamable) {
|
||||
setAllowRename(true);
|
||||
}
|
||||
setRenamable(false);
|
||||
}
|
||||
}}
|
||||
onBlur={async () => {
|
||||
await rename();
|
||||
setAllowRename(false);
|
||||
explorerView.setIsRenaming(false);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...props}
|
||||
>
|
||||
{fileName}
|
||||
</div>
|
||||
);
|
||||
// Reset to original file name
|
||||
function reset() {
|
||||
if (ref?.current) {
|
||||
ref.current.innerText = props.text || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle renaming
|
||||
async function rename(newName: string) {
|
||||
if (!props.locationId) return;
|
||||
try {
|
||||
await renameLocation.mutateAsync({
|
||||
id: props.locationId,
|
||||
name: newName,
|
||||
generate_preview_media: null,
|
||||
sync_preview_media: null,
|
||||
hidden: null,
|
||||
indexer_rules_ids: []
|
||||
});
|
||||
} catch (e) {
|
||||
showAlertDialog({
|
||||
title: 'Error',
|
||||
value: String(e)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return <RenameTextBoxBase {...props} renameHandler={rename} ref={ref} />;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getIcon, iconNames } from '@sd/assets/util';
|
||||
import clsx from 'clsx';
|
||||
import { ImgHTMLAttributes, memo, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { ExplorerItem, useLibraryContext } from '@sd/client';
|
||||
import { ExplorerItem, getItemLocation, useLibraryContext } from '@sd/client';
|
||||
import { PDFViewer } from '~/components';
|
||||
import {
|
||||
getExplorerStore,
|
||||
@@ -52,13 +52,13 @@ const Thumbnail = memo(
|
||||
videoBarsSize
|
||||
? size && size.height >= size.width
|
||||
? {
|
||||
borderLeftWidth: videoBarsSize,
|
||||
borderRightWidth: videoBarsSize
|
||||
}
|
||||
borderLeftWidth: videoBarsSize,
|
||||
borderRightWidth: videoBarsSize
|
||||
}
|
||||
: {
|
||||
borderTopWidth: videoBarsSize,
|
||||
borderBottomWidth: videoBarsSize
|
||||
}
|
||||
borderTopWidth: videoBarsSize,
|
||||
borderBottomWidth: videoBarsSize
|
||||
}
|
||||
: {}
|
||||
}
|
||||
onLoad={props.onLoad}
|
||||
@@ -76,11 +76,11 @@ const Thumbnail = memo(
|
||||
props.cover
|
||||
? {}
|
||||
: size
|
||||
? {
|
||||
? {
|
||||
marginTop: Math.floor(size.height / 2) - 2,
|
||||
marginLeft: Math.floor(size.width / 2) - 2
|
||||
}
|
||||
: { display: 'none' }
|
||||
}
|
||||
: { display: 'none' }
|
||||
}
|
||||
className={clsx(
|
||||
props.cover
|
||||
@@ -101,7 +101,8 @@ const Thumbnail = memo(
|
||||
enum ThumbType {
|
||||
Icon,
|
||||
Original,
|
||||
Thumbnail
|
||||
Thumbnail,
|
||||
Location
|
||||
}
|
||||
|
||||
export interface ThumbProps {
|
||||
@@ -117,6 +118,7 @@ function FileThumb({ size, cover, ...props }: ThumbProps) {
|
||||
const isDark = useIsDark();
|
||||
const platform = usePlatform();
|
||||
const itemData = useExplorerItemData(props.data);
|
||||
const locationData = getItemLocation(props.data);
|
||||
const { library } = useLibraryContext();
|
||||
const [src, setSrc] = useState<null | string>(null);
|
||||
const [loaded, setLoaded] = useState<boolean>(false);
|
||||
@@ -134,10 +136,12 @@ function FileThumb({ size, cover, ...props }: ThumbProps) {
|
||||
setThumbType(ThumbType.Original);
|
||||
} else if (itemData.hasLocalThumbnail) {
|
||||
setThumbType(ThumbType.Thumbnail);
|
||||
} else if (locationData) {
|
||||
setThumbType(ThumbType.Location);
|
||||
} else {
|
||||
setThumbType(ThumbType.Icon);
|
||||
}
|
||||
}, [props.loadOriginal, itemData]);
|
||||
}, [props.loadOriginal, locationData, itemData]);
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
@@ -172,6 +176,9 @@ function FileThumb({ size, cover, ...props }: ThumbProps) {
|
||||
setThumbType(ThumbType.Icon);
|
||||
}
|
||||
break;
|
||||
case ThumbType.Location:
|
||||
setSrc(getIcon('Folder', isDark, extension, true));
|
||||
break;
|
||||
default:
|
||||
if (isDir !== null) setSrc(getIcon(kind, isDark, extension, isDir));
|
||||
break;
|
||||
@@ -208,9 +215,9 @@ function FileThumb({ size, cover, ...props }: ThumbProps) {
|
||||
className={clsx(
|
||||
'relative flex shrink-0 items-center justify-center',
|
||||
size &&
|
||||
kind !== 'Video' &&
|
||||
thumbType !== ThumbType.Icon &&
|
||||
'border-2 border-transparent',
|
||||
kind !== 'Video' &&
|
||||
thumbType !== ThumbType.Icon &&
|
||||
'border-2 border-transparent',
|
||||
size || ['h-full', cover ? 'w-full overflow-hidden' : 'w-[90%]'],
|
||||
props.className
|
||||
)}
|
||||
@@ -312,9 +319,9 @@ function FileThumb({ size, cover, ...props }: ThumbProps) {
|
||||
'shadow shadow-black/30'
|
||||
],
|
||||
size &&
|
||||
(kind === 'Video'
|
||||
? 'border-x-0 border-black'
|
||||
: size > 60 && 'border-2 border-app-line'),
|
||||
(kind === 'Video'
|
||||
? 'border-x-0 border-black'
|
||||
: size > 60 && 'border-2 border-app-line'),
|
||||
props.className
|
||||
)}
|
||||
crossOrigin={ThumbType.Original && 'anonymous'} // Here it is ok, because it is not a react attr
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// import types from '../../constants/file-types.json';
|
||||
import { Image, Image_Light } from '@sd/assets/icons';
|
||||
import byteSize from 'byte-size';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { Barcode, CircleWavyCheck, Clock, Cube, Hash, Link, Lock, Snowflake } from 'phosphor-react';
|
||||
@@ -9,7 +10,9 @@ import {
|
||||
Location,
|
||||
ObjectKind,
|
||||
Tag,
|
||||
formatBytes,
|
||||
bytesToNumber,
|
||||
getItemFilePath,
|
||||
getItemObject,
|
||||
isPath,
|
||||
useLibraryQuery
|
||||
} from '@sd/client';
|
||||
@@ -17,7 +20,6 @@ import { Button, Divider, DropdownMenu, Tooltip, tw } from '@sd/ui';
|
||||
import { useExplorerStore, useIsDark } from '~/hooks';
|
||||
import AssignTagMenuItems from '../AssignTagMenuItems';
|
||||
import FileThumb from '../File/Thumb';
|
||||
import { getItemFilePath, getItemObject } from '../util';
|
||||
import FavoriteButton from './FavoriteButton';
|
||||
import Note from './Note';
|
||||
|
||||
@@ -151,13 +153,17 @@ export const Inspector = ({ data, context, showThumbnail = true, ...props }: Pro
|
||||
</MetaContainer>
|
||||
<Divider />
|
||||
<MetaContainer className="!flex-row space-x-2">
|
||||
<MetaTextLine>
|
||||
<InspectorIcon component={Cube} />
|
||||
<span className="mr-1.5">Size</span>
|
||||
<MetaValue>
|
||||
{formatBytes(Number(filePathData?.size_in_bytes || 0))}
|
||||
</MetaValue>
|
||||
</MetaTextLine>
|
||||
{filePathData?.size_in_bytes_bytes && (
|
||||
<MetaTextLine>
|
||||
<InspectorIcon component={Cube} />
|
||||
<span className="mr-1.5">Size</span>
|
||||
<MetaValue>
|
||||
{byteSize(
|
||||
bytesToNumber(filePathData.size_in_bytes_bytes)
|
||||
).toString()}
|
||||
</MetaValue>
|
||||
</MetaTextLine>
|
||||
)}
|
||||
{fullObjectData.data?.media_data?.duration_seconds && (
|
||||
<MetaTextLine>
|
||||
<InspectorIcon component={Clock} />
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import byteSize from 'byte-size';
|
||||
import clsx from 'clsx';
|
||||
import { memo } from 'react';
|
||||
import { ExplorerItem, formatBytes } from '@sd/client';
|
||||
import { ExplorerItem, bytesToNumber, getItemFilePath, getItemLocation } from '@sd/client';
|
||||
import GridList from '~/components/GridList';
|
||||
import { useExplorerStore } from '~/hooks/useExplorerStore';
|
||||
import { useExplorerStore } from '~/hooks';
|
||||
import { ViewItem } from '.';
|
||||
import RenameTextBox from '../File/RenameTextBox';
|
||||
import FileThumb from '../File/Thumb';
|
||||
import { useExplorerViewContext } from '../ViewContext';
|
||||
import { getItemFilePath } from '../util';
|
||||
import RenamableItemText from './RenamableItemText';
|
||||
|
||||
interface GridViewItemProps {
|
||||
data: ExplorerItem;
|
||||
@@ -16,10 +16,17 @@ interface GridViewItemProps {
|
||||
}
|
||||
|
||||
const GridViewItem = memo(({ data, selected, index, ...props }: GridViewItemProps) => {
|
||||
const filePathData = data ? getItemFilePath(data) : null;
|
||||
const filePathData = getItemFilePath(data);
|
||||
const location = getItemLocation(data);
|
||||
const explorerStore = useExplorerStore();
|
||||
const explorerView = useExplorerViewContext();
|
||||
|
||||
const showSize =
|
||||
!filePathData?.is_dir &&
|
||||
!location &&
|
||||
explorerStore.showBytesInGridView &&
|
||||
(!explorerView.isRenaming || (explorerView.isRenaming && !selected));
|
||||
|
||||
return (
|
||||
<ViewItem data={data} className="h-full w-full" {...props}>
|
||||
<div className={clsx('mb-1 rounded-lg ', selected && 'bg-app-selectedItem')}>
|
||||
@@ -27,30 +34,16 @@ const GridViewItem = memo(({ data, selected, index, ...props }: GridViewItemProp
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center">
|
||||
{filePathData && (
|
||||
<RenameTextBox
|
||||
filePathData={filePathData}
|
||||
disabled={!selected}
|
||||
<RenamableItemText item={data} selected={selected} />
|
||||
{showSize && filePathData?.size_in_bytes_bytes && (
|
||||
<span
|
||||
className={clsx(
|
||||
'text-center font-medium text-ink',
|
||||
selected && 'bg-accent text-white dark:text-ink'
|
||||
'cursor-default truncate rounded-md px-1.5 py-[1px] text-center text-tiny text-ink-dull '
|
||||
)}
|
||||
style={{
|
||||
maxHeight: explorerStore.gridItemSize / 3
|
||||
}}
|
||||
activeClassName="!text-ink"
|
||||
/>
|
||||
>
|
||||
{byteSize(bytesToNumber(filePathData.size_in_bytes_bytes)).toString()}
|
||||
</span>
|
||||
)}
|
||||
{explorerStore.showBytesInGridView &&
|
||||
(!explorerView.isRenaming || (explorerView.isRenaming && !selected)) && (
|
||||
<span
|
||||
className={clsx(
|
||||
'cursor-default truncate rounded-md px-1.5 py-[1px] text-center text-tiny text-ink-dull '
|
||||
)}
|
||||
>
|
||||
{formatBytes(Number(filePathData?.size_in_bytes || 0))}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</ViewItem>
|
||||
);
|
||||
@@ -81,19 +74,7 @@ export default () => {
|
||||
preventContextMenuSelection={!explorerView.contextMenu}
|
||||
>
|
||||
{({ index, item: Item }) => {
|
||||
if (!explorerView.items) {
|
||||
return (
|
||||
<Item className="p-px">
|
||||
<div className="aspect-square animate-pulse rounded-md bg-app-box" />
|
||||
<div className="mx-2 mt-3 h-2 animate-pulse rounded bg-app-box" />
|
||||
{explorerStore.showBytesInGridView && (
|
||||
<div className="mx-8 mt-2 h-1 animate-pulse rounded bg-app-box" />
|
||||
)}
|
||||
</Item>
|
||||
);
|
||||
}
|
||||
|
||||
const item = explorerView.items[index];
|
||||
const item = explorerView.items?.[index];
|
||||
if (!item) return null;
|
||||
|
||||
const isSelected = Array.isArray(explorerView.selected)
|
||||
|
||||
@@ -15,19 +15,28 @@ import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ScrollSync, ScrollSyncPane } from 'react-scroll-sync';
|
||||
import { useBoundingclientrect, useKey } from 'rooks';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import { ExplorerItem, FilePath, ObjectKind, isObject, isPath } from '@sd/client';
|
||||
import {
|
||||
ExplorerItem,
|
||||
FilePath,
|
||||
ObjectKind,
|
||||
bytesToNumber,
|
||||
getExplorerItemData,
|
||||
getItemFilePath,
|
||||
getItemLocation,
|
||||
getItemObject,
|
||||
isPath
|
||||
} from '@sd/client';
|
||||
import {
|
||||
FilePathSearchOrderingKeys,
|
||||
getExplorerStore,
|
||||
useExplorerStore
|
||||
} from '~/hooks/useExplorerStore';
|
||||
import { useScrolled } from '~/hooks/useScrolled';
|
||||
useExplorerStore,
|
||||
useScrolled
|
||||
} from '~/hooks';
|
||||
import { ViewItem } from '.';
|
||||
import RenameTextBox from '../File/RenameTextBox';
|
||||
import FileThumb from '../File/Thumb';
|
||||
import { InfoPill } from '../Inspector';
|
||||
import { useExplorerViewContext } from '../ViewContext';
|
||||
import { getExplorerItemData, getItemFilePath } from '../util';
|
||||
import RenamableItemText from './RenamableItemText';
|
||||
|
||||
interface ListViewItemProps {
|
||||
row: Row<ExplorerItem>;
|
||||
@@ -105,7 +114,6 @@ export default () => {
|
||||
const { width: tableWidth = 0 } = useResizeObserver({ ref: tableRef });
|
||||
const { width: headerWidth = 0 } = useResizeObserver({ ref: tableHeaderRef });
|
||||
|
||||
const getObjectData = (data: ExplorerItem) => (isObject(data) ? data.item : data.item.object);
|
||||
const getFileName = (path: FilePath) => `${path.name}${path.extension && `.${path.extension}`}`;
|
||||
|
||||
const columns = useMemo<ColumnDef<ExplorerItem>[]>(
|
||||
@@ -116,12 +124,14 @@ export default () => {
|
||||
minSize: 200,
|
||||
meta: { className: '!overflow-visible !text-ink' },
|
||||
accessorFn: (file) => {
|
||||
const locationData = getItemLocation(file);
|
||||
const filePathData = getItemFilePath(file);
|
||||
return filePathData && getFileName(filePathData);
|
||||
return locationData
|
||||
? locationData.name
|
||||
: filePathData && getFileName(filePathData);
|
||||
},
|
||||
cell: (cell) => {
|
||||
const file = cell.row.original;
|
||||
const filePathData = getItemFilePath(file);
|
||||
|
||||
const selectedId = Array.isArray(explorerView.selected)
|
||||
? explorerView.selected[0]
|
||||
@@ -134,17 +144,16 @@ export default () => {
|
||||
<div className="mr-[10px] flex h-6 w-12 shrink-0 items-center justify-center">
|
||||
<FileThumb data={file} size={35} />
|
||||
</div>
|
||||
{filePathData && (
|
||||
<RenameTextBox
|
||||
filePathData={filePathData}
|
||||
disabled={
|
||||
!selected ||
|
||||
(Array.isArray(explorerView.selected) &&
|
||||
explorerView.selected.length > 1)
|
||||
}
|
||||
activeClassName="absolute z-50 top-0.5 left-[58px] max-w-[calc(100%-60px)]"
|
||||
/>
|
||||
)}
|
||||
<RenamableItemText
|
||||
allowHighlight={false}
|
||||
item={file}
|
||||
selected={selected}
|
||||
disabled={
|
||||
!selected ||
|
||||
(Array.isArray(explorerView.selected) &&
|
||||
explorerView.selected.length > 1)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -156,7 +165,7 @@ export default () => {
|
||||
accessorFn: (file) => {
|
||||
return isPath(file) && file.item.is_dir
|
||||
? 'Folder'
|
||||
: ObjectKind[getObjectData(file)?.kind || 0];
|
||||
: ObjectKind[getItemObject(file)?.kind || 0];
|
||||
},
|
||||
cell: (cell) => {
|
||||
const file = cell.row.original;
|
||||
@@ -164,7 +173,7 @@ export default () => {
|
||||
<InfoPill className="bg-app-button/50">
|
||||
{isPath(file) && file.item.is_dir
|
||||
? 'Folder'
|
||||
: ObjectKind[getObjectData(file)?.kind || 0]}
|
||||
: ObjectKind[getItemObject(file)?.kind || 0]}
|
||||
</InfoPill>
|
||||
);
|
||||
}
|
||||
@@ -173,18 +182,47 @@ export default () => {
|
||||
id: 'sizeInBytes',
|
||||
header: 'Size',
|
||||
size: 100,
|
||||
accessorFn: (file) => byteSize(Number(getItemFilePath(file)?.size_in_bytes || 0))
|
||||
accessorFn: (file) => {
|
||||
const file_path = getItemFilePath(file);
|
||||
if (!file_path || !file_path.size_in_bytes_bytes) return;
|
||||
|
||||
return byteSize(bytesToNumber(file_path.size_in_bytes_bytes));
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'dateCreated',
|
||||
header: 'Date Created',
|
||||
accessorFn: (file) => dayjs(file.item.date_created).format('MMM Do YYYY')
|
||||
},
|
||||
{
|
||||
id: 'dateModified',
|
||||
header: 'Date Modified',
|
||||
accessorFn: (file) =>
|
||||
dayjs(getItemFilePath(file)?.date_modified).format('MMM Do YYYY')
|
||||
},
|
||||
{
|
||||
id: 'dateIndexed',
|
||||
header: 'Date Indexed',
|
||||
accessorFn: (file) =>
|
||||
dayjs(getItemFilePath(file)?.date_indexed).format('MMM Do YYYY')
|
||||
},
|
||||
{
|
||||
id: 'dateAccessed',
|
||||
header: 'Date Accessed',
|
||||
accessorFn: (file) =>
|
||||
dayjs(getItemObject(file)?.date_accessed).format('MMM Do YYYY')
|
||||
},
|
||||
{
|
||||
header: 'Content ID',
|
||||
enableSorting: false,
|
||||
size: 180,
|
||||
accessorFn: (file) => getExplorerItemData(file).casId
|
||||
},
|
||||
{
|
||||
header: 'Object ID',
|
||||
enableSorting: false,
|
||||
size: 180,
|
||||
accessorFn: (file) => getItemObject(file)?.pub_id
|
||||
}
|
||||
],
|
||||
[explorerView.selected]
|
||||
|
||||
54
interface/app/$libraryId/Explorer/View/RenamableItemText.tsx
Normal file
54
interface/app/$libraryId/Explorer/View/RenamableItemText.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import clsx from 'clsx';
|
||||
import { ExplorerItem, getItemFilePath, getItemLocation } from '@sd/client';
|
||||
import { useExplorerStore } from '~/hooks';
|
||||
import { RenameLocationTextBox, RenamePathTextBox } from '../File/RenameTextBox';
|
||||
|
||||
export default function RenamableItemText(props: {
|
||||
item: ExplorerItem;
|
||||
selected: boolean;
|
||||
disabled?: boolean;
|
||||
allowHighlight?: boolean;
|
||||
}) {
|
||||
const { item, selected, disabled, allowHighlight } = props;
|
||||
const explorerStore = useExplorerStore();
|
||||
|
||||
const sharedProps = {
|
||||
className: clsx(
|
||||
'text-center font-medium text-ink',
|
||||
selected && allowHighlight !== false && 'bg-accent text-white dark:text-ink'
|
||||
),
|
||||
style: { maxHeight: explorerStore.gridItemSize / 3 },
|
||||
activeClassName: '!text-ink',
|
||||
disabled: !selected || disabled
|
||||
};
|
||||
|
||||
switch (item.type) {
|
||||
case 'Path':
|
||||
case 'Object':
|
||||
const filePathData = getItemFilePath(item);
|
||||
if (!filePathData) break;
|
||||
return (
|
||||
<RenamePathTextBox
|
||||
itemId={filePathData.id}
|
||||
text={filePathData.name}
|
||||
extension={filePathData.extension}
|
||||
isDir={filePathData.is_dir || false}
|
||||
locationId={filePathData.location_id}
|
||||
{...sharedProps}
|
||||
/>
|
||||
);
|
||||
case 'Location':
|
||||
const locationData = getItemLocation(item);
|
||||
if (!locationData) break;
|
||||
return (
|
||||
<RenameLocationTextBox
|
||||
locationId={locationData.id}
|
||||
itemId={locationData.id}
|
||||
text={locationData.name}
|
||||
{...sharedProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <div />;
|
||||
}
|
||||
@@ -10,14 +10,17 @@ import {
|
||||
} from 'react';
|
||||
import { createSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { useKey } from 'rooks';
|
||||
import { ExplorerItem, isPath, useLibraryContext, useLibraryMutation } from '@sd/client';
|
||||
import { ContextMenu } from '@sd/ui';
|
||||
import {
|
||||
ExplorerLayoutMode,
|
||||
getExplorerStore,
|
||||
useExplorerConfigStore,
|
||||
useExplorerStore
|
||||
} from '~/hooks';
|
||||
ExplorerItem,
|
||||
getExplorerItemData,
|
||||
getItemFilePath,
|
||||
getItemLocation,
|
||||
isPath,
|
||||
useLibraryContext,
|
||||
useLibraryMutation
|
||||
} from '@sd/client';
|
||||
import { ContextMenu } from '@sd/ui';
|
||||
import { ExplorerLayoutMode, getExplorerStore, useExplorerConfigStore } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import {
|
||||
ExplorerViewContext,
|
||||
@@ -26,7 +29,6 @@ import {
|
||||
ViewContext,
|
||||
useExplorerViewContext
|
||||
} from '../ViewContext';
|
||||
import { getExplorerItemData, getItemFilePath } from '../util';
|
||||
import GridView from './GridView';
|
||||
import ListView from './ListView';
|
||||
import MediaView from './MediaView';
|
||||
@@ -43,11 +45,19 @@ export const ViewItem = ({ data, children, ...props }: ViewItemProps) => {
|
||||
const { openFilePath } = usePlatform();
|
||||
const updateAccessTime = useLibraryMutation('files.updateAccessTime');
|
||||
const filePath = getItemFilePath(data);
|
||||
const location = getItemLocation(data);
|
||||
|
||||
const explorerConfig = useExplorerConfigStore();
|
||||
|
||||
const onDoubleClick = () => {
|
||||
if (isPath(data) && data.item.is_dir) {
|
||||
if (location) {
|
||||
navigate({
|
||||
pathname: `/${library.uuid}/location/${location.id}`,
|
||||
search: createSearchParams({
|
||||
path: `/`
|
||||
}).toString()
|
||||
});
|
||||
} else if (isPath(data) && data.item.is_dir) {
|
||||
navigate({
|
||||
pathname: `/${library.uuid}/location/${getItemFilePath(data)?.location_id}`,
|
||||
search: createSearchParams({
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
ExplorerItem,
|
||||
FilePathSearchOrdering,
|
||||
ObjectKind,
|
||||
ObjectKindKey,
|
||||
isObject,
|
||||
isPath
|
||||
} from '@sd/client';
|
||||
import { FilePathSearchOrdering } from '@sd/client';
|
||||
import { useExplorerStore, useZodSearchParams } from '~/hooks';
|
||||
|
||||
export function useExplorerOrder(): FilePathSearchOrdering | undefined {
|
||||
@@ -31,29 +24,6 @@ export function useExplorerOrder(): FilePathSearchOrdering | undefined {
|
||||
return ordering;
|
||||
}
|
||||
|
||||
export function getItemObject(data: ExplorerItem) {
|
||||
return isObject(data) ? data.item : data.item.object;
|
||||
}
|
||||
|
||||
export function getItemFilePath(data: ExplorerItem) {
|
||||
return isObject(data) ? data.item.file_paths[0] : data.item;
|
||||
}
|
||||
|
||||
export function getExplorerItemData(data: ExplorerItem) {
|
||||
const filePath = getItemFilePath(data);
|
||||
const objectData = getItemObject(data);
|
||||
|
||||
return {
|
||||
kind: (ObjectKind[objectData?.kind ?? 0] as ObjectKindKey) || null,
|
||||
casId: filePath?.cas_id || null,
|
||||
isDir: isPath(data) && data.item.is_dir,
|
||||
extension: filePath?.extension || null,
|
||||
locationId: filePath?.location_id || null,
|
||||
hasLocalThumbnail: data.has_local_thumbnail, // this will be overwritten if new thumbnail is generated
|
||||
thumbnailKey: data.thumbnail_key
|
||||
};
|
||||
}
|
||||
|
||||
export const SEARCH_PARAMS = z.object({
|
||||
path: z.string().optional(),
|
||||
take: z.coerce.number().default(100)
|
||||
|
||||
@@ -1,85 +1,26 @@
|
||||
// import { Gear, Lock, MagnifyingGlass, X } from 'phosphor-react';
|
||||
// import { useLibraryContext, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||
// import { Button, Tabs } from '@sd/ui';
|
||||
// import KeyList from './List';
|
||||
// import KeyMounter from './Mounter';
|
||||
// import NotSetup from './NotSetup';
|
||||
// import NotUnlocked from './NotUnlocked';
|
||||
/* eslint-disable tailwindcss/classnames-order */
|
||||
import { Keys } from '@sd/assets/icons';
|
||||
import { Button, Tooltip } from '@sd/ui';
|
||||
|
||||
// export function KeyManager() {
|
||||
// const isUnlocked = useLibraryQuery(['keys.isUnlocked']);
|
||||
// const isSetup = useLibraryQuery(['keys.isSetup']);
|
||||
export function KeyManager() {
|
||||
// const isUnlocked = useLibraryQuery(['keys.isUnlocked']);
|
||||
// const isSetup = useLibraryQuery(['keys.isSetup']);
|
||||
|
||||
// if (!isSetup?.data) return <NotSetup />;
|
||||
// if (!isUnlocked?.data) return <NotUnlocked />;
|
||||
// else return <Unlocked />;
|
||||
// }
|
||||
|
||||
// const Unlocked = () => {
|
||||
// const { library } = useLibraryContext();
|
||||
// const isUnlocked = useLibraryQuery(['keys.isUnlocked']);
|
||||
|
||||
// const unmountAll = useLibraryMutation('keys.unmountAll');
|
||||
// const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword');
|
||||
|
||||
// return (
|
||||
// <div className="w-[350px]">
|
||||
// <Tabs.Root defaultValue="keys">
|
||||
// <div className="min-w-32 flex flex-col">
|
||||
// <Tabs.List>
|
||||
// {/* <Input placeholder="Search" /> */}
|
||||
// {/* <Tabs.Trigger className="!rounded-md text-sm font-medium" value="mount">
|
||||
// Mount
|
||||
// </Tabs.Trigger>
|
||||
// <Tabs.Trigger className="!rounded-md text-sm font-medium" value="keys">
|
||||
// Keys
|
||||
// </Tabs.Trigger> */}
|
||||
// <Button size="icon" variant="subtle" className="text-ink-faint">
|
||||
// <MagnifyingGlass className="h-4 w-4 text-ink-faint" />
|
||||
// </Button>
|
||||
// <div className="grow" />
|
||||
// <Button
|
||||
// size="icon"
|
||||
// onClick={() => {
|
||||
// unmountAll
|
||||
// .mutateAsync(null)
|
||||
// .then(() => clearMasterPassword.mutateAsync(null))
|
||||
// .then(() => isUnlocked.refetch());
|
||||
// }}
|
||||
// variant="subtle"
|
||||
// className="text-ink-faint"
|
||||
// >
|
||||
// <Lock className="h-4 w-4 text-ink-faint" />
|
||||
// </Button>
|
||||
// <Button size="icon" variant="subtle" className="text-ink-faint">
|
||||
// <Gear className="h-4 w-4 text-ink-faint" />
|
||||
// </Button>
|
||||
// <Button size="icon" variant="subtle" className="text-ink-faint">
|
||||
// <X className="h-4 w-4 text-ink-faint" />
|
||||
// </Button>
|
||||
// </Tabs.List>
|
||||
// </div>
|
||||
// <Tabs.Content value="keys">
|
||||
// <Keys />
|
||||
// </Tabs.Content>
|
||||
// <Tabs.Content value="mount">
|
||||
// <KeyMounter />
|
||||
// </Tabs.Content>
|
||||
// </Tabs.Root>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// const Keys = () => {
|
||||
// return (
|
||||
// <div className="flex h-full max-h-[360px] flex-col">
|
||||
// <div className="custom-scroll overlay-scroll p-3">
|
||||
// <div className="">
|
||||
// <div className="space-y-1.5">
|
||||
// <KeyList />
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
return (
|
||||
<div className="flex h-full max-w-[300px] flex-col">
|
||||
<div className="flex w-full flex-col items-center p-4">
|
||||
<img src={Keys} className="h-14 w-14" />
|
||||
<span className="text-lg font-bold">Key Manager</span>
|
||||
<span className="mt-2 text-center text-ink-dull">
|
||||
Create encryption keys, mount and unmount your keys to see files decrypted on
|
||||
the fly.
|
||||
</span>
|
||||
<Tooltip className="w-full" label="Coming soon!">
|
||||
<Button disabled className="mt-4 w-full" variant="accent">
|
||||
Set up
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,26 +25,14 @@ export default () => {
|
||||
<Icon component={Planet} />
|
||||
Overview
|
||||
</SidebarLink>
|
||||
{/* <SidebarLink disabled to="spaces">
|
||||
<Icon component={CirclesFour} />
|
||||
Spaces
|
||||
</SidebarLink> */}
|
||||
<SidebarLink to="spacedrop" disabled>
|
||||
{/* <SidebarLink to="spacedrop">
|
||||
<Icon component={Broadcast} />
|
||||
Spacedrop
|
||||
</SidebarLink>
|
||||
{/* <SidebarLink disabled to="media">
|
||||
<Icon component={MonitorPlay} />
|
||||
Media
|
||||
</SidebarLink> */}
|
||||
<SidebarLink to="imports" disabled>
|
||||
<SidebarLink to="imports">
|
||||
<Icon component={ArchiveBox} />
|
||||
Imports
|
||||
</SidebarLink>
|
||||
{/* <SidebarLink to="sync"> */}
|
||||
{/* <Icon component={ArrowsClockwise} /> */}
|
||||
{/* Sync */}
|
||||
{/* </SidebarLink> */}
|
||||
</SidebarLink> */}
|
||||
</div>
|
||||
{library && <LibrarySection />}
|
||||
<Section name="Tools" actionArea={<SubtleButton />}>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useLibraryQuery,
|
||||
useOnlineLocations
|
||||
} from '@sd/client';
|
||||
import { Button, Tooltip } from '@sd/ui';
|
||||
import { AddLocationButton } from '~/app/$libraryId/settings/library/locations/AddLocationButton';
|
||||
import { Folder } from '~/components/Folder';
|
||||
import { SubtleButton } from '~/components/SubtleButton';
|
||||
@@ -27,9 +28,11 @@ type TriggeredContextItem =
|
||||
tagId: number;
|
||||
};
|
||||
|
||||
const SEE_MORE_LOCATIONS_COUNT = 5;
|
||||
|
||||
export const LibrarySection = () => {
|
||||
const node = useBridgeQuery(['nodeState']);
|
||||
const locations = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
const tags = useLibraryQuery(['tags.list'], { keepPreviousData: true });
|
||||
const onlineLocations = useOnlineLocations();
|
||||
const isPairingEnabled = useFeatureFlag('p2pPairing');
|
||||
@@ -37,6 +40,13 @@ export const LibrarySection = () => {
|
||||
null
|
||||
);
|
||||
|
||||
const [seeMoreLocations, setSeeMoreLocations] = useState(false);
|
||||
|
||||
const locations = locationsQuery.data?.slice(
|
||||
0,
|
||||
seeMoreLocations ? undefined : SEE_MORE_LOCATIONS_COUNT
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const outsideClick = () => {
|
||||
document.addEventListener('click', () => {
|
||||
@@ -63,27 +73,25 @@ export const LibrarySection = () => {
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* <SidebarLink className="relative w-full group" to={`/`}>
|
||||
<img src={Laptop} className="w-5 h-5 mr-1" />
|
||||
<span className="truncate">Jamie's MBP</span>
|
||||
</SidebarLink>
|
||||
<SidebarLink className="relative w-full group" to={`/`}>
|
||||
<img src={Mobile} className="w-5 h-5 mr-1" />
|
||||
<span className="truncate">spacephone</span>
|
||||
</SidebarLink>
|
||||
<SidebarLink className="relative w-full group" to={`/`}>
|
||||
<img src={Server} className="w-5 h-5 mr-1" />
|
||||
<span className="truncate">titan</span>
|
||||
</SidebarLink>
|
||||
{(locations.data?.length || 0) < 4 && (
|
||||
<Button variant="dotted" className="w-full mt-1">
|
||||
{node.data && (
|
||||
<SidebarLink
|
||||
className="group relative w-full"
|
||||
to={`node/${node.data.id}`}
|
||||
key={node.data.id}
|
||||
>
|
||||
<img src={Laptop} className="mr-1 h-5 w-5" />
|
||||
<span className="truncate">{node.data.name}</span>
|
||||
</SidebarLink>
|
||||
)}
|
||||
<Tooltip
|
||||
label="Coming soon! This alpha release doesn't include library sync, it will be ready very soon."
|
||||
tooltipClassName="bg-black"
|
||||
position="right"
|
||||
>
|
||||
<Button disabled variant="dotted" className="mt-1 w-full">
|
||||
Connect Node
|
||||
</Button>
|
||||
)} */}
|
||||
<SidebarLink disabled className="group relative w-full" to={`/`} key={'jeff'}>
|
||||
<img src={Laptop} className="mr-1 h-5 w-5" />
|
||||
<span className="truncate">{node.data?.name}</span>
|
||||
</SidebarLink>
|
||||
</Tooltip>
|
||||
</Section>
|
||||
<Section
|
||||
name="Locations"
|
||||
@@ -93,7 +101,7 @@ export const LibrarySection = () => {
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
{locations.data?.map((location) => {
|
||||
{locations?.map((location) => {
|
||||
const online = onlineLocations?.some((l) => arraysEqual(location.pub_id, l));
|
||||
return (
|
||||
<LocationsContextMenu key={location.id} locationId={location.id}>
|
||||
@@ -128,7 +136,15 @@ export const LibrarySection = () => {
|
||||
</LocationsContextMenu>
|
||||
);
|
||||
})}
|
||||
{(locations.data?.length || 0) < 4 && <AddLocationButton className="mt-1" />}
|
||||
{locationsQuery.data?.[SEE_MORE_LOCATIONS_COUNT - 1] && (
|
||||
<div
|
||||
onClick={() => setSeeMoreLocations(!seeMoreLocations)}
|
||||
className="mb-1 ml-2 mt-0.5 cursor-pointer text-center text-tiny font-semibold text-ink-faint/50 transition hover:text-accent"
|
||||
>
|
||||
See {seeMoreLocations ? 'less' : 'more'}
|
||||
</div>
|
||||
)}
|
||||
<AddLocationButton className="mt-1" />
|
||||
</Section>
|
||||
{!!tags.data?.length && (
|
||||
<Section
|
||||
|
||||
@@ -22,6 +22,7 @@ const pageRoutes: RouteObject = {
|
||||
// provided by PageLayout
|
||||
const explorerRoutes: RouteObject[] = [
|
||||
{ path: 'location/:id', lazy: () => import('./location/$id') },
|
||||
{ path: 'node/:id', lazy: () => import('./node/$id') },
|
||||
{ path: 'tag/:id', lazy: () => import('./tag/$id') },
|
||||
{ path: 'search', lazy: () => import('./search') }
|
||||
];
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
useLibrarySubscription,
|
||||
useRspcLibraryContext
|
||||
} from '@sd/client';
|
||||
import { Folder } from '~/components/Folder';
|
||||
import { Folder } from '~/components';
|
||||
import {
|
||||
getExplorerStore,
|
||||
useExplorerStore,
|
||||
@@ -59,7 +59,9 @@ export const Component = () => {
|
||||
<span className="flex flex-row items-center">
|
||||
<Folder size={22} className="ml-3 mr-2 mt-[-1px] inline-block" />
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium">
|
||||
{path ? getLastSectionOfPath(path) : location.data?.name}
|
||||
{path && path?.length > 1
|
||||
? getLastSectionOfPath(path)
|
||||
: location.data?.name}
|
||||
</span>
|
||||
</span>
|
||||
{location.data && (
|
||||
|
||||
48
interface/app/$libraryId/node/$id.tsx
Normal file
48
interface/app/$libraryId/node/$id.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Node } from '@sd/assets/icons';
|
||||
import { z } from 'zod';
|
||||
import { useBridgeQuery, useLibraryQuery } from '@sd/client';
|
||||
import { useExplorerTopBarOptions, useZodRouteParams } from '~/hooks';
|
||||
import Explorer from '../Explorer';
|
||||
import { useExplorerSearchParams } from '../Explorer/util';
|
||||
import { TopBarPortal } from '../TopBar/Portal';
|
||||
import TopBarOptions from '../TopBar/TopBarOptions';
|
||||
|
||||
const PARAMS = z.object({
|
||||
id: z.string()
|
||||
});
|
||||
|
||||
export const Component = () => {
|
||||
// const [{ path }] = useExplorerSearchParams();
|
||||
const { id: node_id } = useZodRouteParams(PARAMS);
|
||||
|
||||
const locations = useLibraryQuery(['nodes.listLocations', node_id]);
|
||||
|
||||
const nodeState = useBridgeQuery(['nodeState']);
|
||||
|
||||
const { explorerViewOptions, explorerControlOptions, explorerToolOptions } =
|
||||
useExplorerTopBarOptions();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopBarPortal
|
||||
left={
|
||||
<div className="group flex flex-row items-center space-x-2">
|
||||
<span className="flex flex-row items-center">
|
||||
<img src={Node} className="ml-3 mr-2 mt-[-1px] inline-block h-7 w-7" />
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium">
|
||||
{nodeState.data?.name || 'Node'}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
right={
|
||||
<TopBarOptions
|
||||
options={[explorerViewOptions, explorerToolOptions, explorerControlOptions]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{locations.data && <Explorer items={locations.data} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -69,7 +69,7 @@ export const Categories = (props: { selected: Category; onSelectedChanged(c: Cat
|
||||
scroll > 0
|
||||
? 'cursor-pointer bg-app/50 opacity-100 hover:opacity-95'
|
||||
: 'pointer-events-none',
|
||||
'sticky left-[15px] z-40 mt-4 flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-app-line bg-app p-2 opacity-0 backdrop-blur-md transition-all duration-200'
|
||||
'sticky left-[15px] z-40 -ml-4 mt-4 flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-app-line bg-app p-2 opacity-0 backdrop-blur-md transition-all duration-200'
|
||||
)}
|
||||
>
|
||||
<ArrowLeft weight="bold" className="h-4 w-4 text-ink" />
|
||||
|
||||
@@ -6,15 +6,17 @@ interface CategoryButtonProps {
|
||||
icon: string;
|
||||
selected?: boolean;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default ({ category, icon, items, selected, onClick }: CategoryButtonProps) => {
|
||||
export default ({ category, icon, items, selected, onClick, disabled }: CategoryButtonProps) => {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={clsx(
|
||||
'flex shrink-0 items-center rounded-md px-1.5 py-1 text-sm outline-none focus:bg-app-selectedItem/50',
|
||||
selected && 'bg-app-selectedItem'
|
||||
'flex shrink-0 items-center rounded-lg px-1.5 py-1 text-sm outline-none focus:bg-app-selectedItem/50',
|
||||
selected && 'bg-app-selectedItem',
|
||||
disabled && 'cursor-not-allowed opacity-30'
|
||||
)}
|
||||
>
|
||||
<img src={icon} className="mr-3 h-12 w-12" />
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import byteSize from 'byte-size';
|
||||
import clsx from 'clsx';
|
||||
import { Info } from 'phosphor-react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import 'react-loading-skeleton/dist/skeleton.css';
|
||||
import { Statistics, useLibraryContext, useLibraryQuery } from '@sd/client';
|
||||
import { Tooltip } from '@sd/ui';
|
||||
import { useCounter } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
@@ -10,6 +12,7 @@ interface StatItemProps {
|
||||
title: string;
|
||||
bytes: bigint;
|
||||
isLoading: boolean;
|
||||
info?: string;
|
||||
}
|
||||
|
||||
const StatItemNames: Partial<Record<keyof Statistics, string>> = {
|
||||
@@ -18,6 +21,13 @@ const StatItemNames: Partial<Record<keyof Statistics, string>> = {
|
||||
library_db_size: 'Index size',
|
||||
total_bytes_free: 'Free space'
|
||||
};
|
||||
const StatDescriptions: Partial<Record<keyof Statistics, string>> = {
|
||||
total_bytes_capacity:
|
||||
'The total capacity of all nodes connected to the library. May show incorrect values during alpha.',
|
||||
preview_media_bytes: 'The total size of all preview media files, such as thumbnails.',
|
||||
library_db_size: 'The size of the library database.',
|
||||
total_bytes_free: 'Free space available on all nodes connected to the library.'
|
||||
};
|
||||
|
||||
const EMPTY_STATISTICS = {
|
||||
id: 0,
|
||||
@@ -49,11 +59,22 @@ const StatItem = (props: StatItemProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-32 shrink-0 cursor-default flex-col rounded-md px-4 py-3 duration-75',
|
||||
'group flex w-32 shrink-0 cursor-default flex-col rounded-md px-4 py-3 duration-75',
|
||||
!bytes && 'hidden'
|
||||
)}
|
||||
>
|
||||
<span className="text-sm text-gray-400">{title}</span>
|
||||
<span className="whitespace-nowrap text-sm text-gray-400 ">
|
||||
{title}
|
||||
{props.info && (
|
||||
<Tooltip tooltipClassName="bg-black" label={props.info}>
|
||||
<Info
|
||||
weight="fill"
|
||||
className="-mt-0.5 ml-1 inline h-3 w-3 text-ink-faint opacity-0 transition-opacity group-hover:opacity-70"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span className="text-2xl">
|
||||
{isLoading && (
|
||||
<div>
|
||||
@@ -97,6 +118,7 @@ export default () => {
|
||||
title={StatItemNames[key as keyof Statistics]!}
|
||||
bytes={BigInt(value)}
|
||||
isLoading={platform.demoMode ? false : stats.isLoading}
|
||||
info={StatDescriptions[key as keyof Statistics]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MagnifyingGlass } from 'phosphor-react';
|
||||
import { Suspense, memo, useDeferredValue, useEffect, useMemo } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { useLibraryQuery } from '@sd/client';
|
||||
import { getExplorerItemData, useLibraryQuery } from '@sd/client';
|
||||
import {
|
||||
SortOrder,
|
||||
getExplorerStore,
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
useZodSearchParams
|
||||
} from '~/hooks';
|
||||
import Explorer from './Explorer';
|
||||
import { getExplorerItemData } from './Explorer/util';
|
||||
import { TopBarPortal } from './TopBar/Portal';
|
||||
import TopBarOptions from './TopBar/TopBarOptions';
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Books,
|
||||
Cloud,
|
||||
FlyingSaucer,
|
||||
GearSix,
|
||||
HardDrive,
|
||||
@@ -70,10 +71,10 @@ export default () => {
|
||||
<Icon component={GearSix} />
|
||||
General
|
||||
</SidebarLink>
|
||||
<SidebarLink to="library/nodes" disabled={!isPairingEnabled}>
|
||||
{/* <SidebarLink to="library/nodes" disabled={!isPairingEnabled}>
|
||||
<Icon component={ShareNetwork} />
|
||||
Nodes
|
||||
</SidebarLink>
|
||||
</SidebarLink> */}
|
||||
<SidebarLink to="library/locations">
|
||||
<Icon component={HardDrive} />
|
||||
Locations
|
||||
@@ -82,6 +83,10 @@ export default () => {
|
||||
<Icon component={TagSimple} />
|
||||
Tags
|
||||
</SidebarLink>
|
||||
<SidebarLink disabled to="library/clouds">
|
||||
<Icon component={Cloud} />
|
||||
Clouds
|
||||
</SidebarLink>
|
||||
<SidebarLink to="library/keys" disabled>
|
||||
<Icon component={Key} />
|
||||
Keys
|
||||
|
||||
@@ -165,7 +165,7 @@ export const Component = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{themeStore.theme === 'dark' && (
|
||||
{/* {themeStore.theme === 'dark' && (
|
||||
<Setting mini title="Theme hue value" description="Change the hue of the theme">
|
||||
<div className="mr-3 w-full max-w-[200px] justify-between gap-5">
|
||||
<div className="w-full">
|
||||
@@ -183,25 +183,27 @@ export const Component = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Setting>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
<Setting
|
||||
mini
|
||||
title="UI Animations"
|
||||
className="opacity-30"
|
||||
description="Dialogs and other UI elements will animate when opening and closing."
|
||||
>
|
||||
<Switch disabled {...form.register('uiAnimations')} className="m-2 ml-4" />
|
||||
</Setting>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Setting
|
||||
mini
|
||||
title="UI Animations"
|
||||
className="opacity-30"
|
||||
description="Dialogs and other UI elements will animate when opening and closing."
|
||||
>
|
||||
<Switch disabled {...form.register('uiAnimations')} className="m-2 ml-4" />
|
||||
</Setting>
|
||||
|
||||
<Setting
|
||||
mini
|
||||
title="Blur Effects"
|
||||
className="opacity-30"
|
||||
description="Some components will have a blur effect applied to them."
|
||||
>
|
||||
<Switch disabled {...form.register('blurEffects')} className="m-2 ml-4" />
|
||||
</Setting>
|
||||
<Setting
|
||||
mini
|
||||
title="Blur Effects"
|
||||
className="opacity-30"
|
||||
description="Some components will have a blur effect applied to them."
|
||||
>
|
||||
<Switch disabled {...form.register('blurEffects')} className="m-2 ml-4" />
|
||||
</Setting>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Laptop, Node } from '@sd/assets/icons';
|
||||
import { Database } from 'phosphor-react';
|
||||
import { getDebugState, useBridgeQuery, useDebugState } from '@sd/client';
|
||||
import { Card, Input, Switch, tw } from '@sd/ui';
|
||||
import { Button, Card, Input, Label, Switch, tw } from '@sd/ui';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
import { Heading } from '../Layout';
|
||||
import Setting from '../Setting';
|
||||
@@ -29,8 +30,10 @@ export const Component = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="mb-4 mt-2 border-app-line" />
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<hr className="mb-4 mt-2 flex w-full border-app-line" />
|
||||
<div className="flex w-full items-center gap-5">
|
||||
<img src={Node} className="mt-2 h-14 w-14" />
|
||||
|
||||
<div className="flex flex-col">
|
||||
<NodeSettingLabel>Node Name</NodeSettingLabel>
|
||||
<Input
|
||||
@@ -38,7 +41,6 @@ export const Component = () => {
|
||||
onChange={() => {
|
||||
/* TODO */
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
@@ -49,18 +51,12 @@ export const Component = () => {
|
||||
onChange={() => {
|
||||
/* TODO */
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center space-x-3">
|
||||
<Switch size="sm" checked />
|
||||
<span className="text-sm font-medium text-ink-dull">
|
||||
Run daemon when app closed
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<div
|
||||
|
||||
<div className="mt-6 gap-2">
|
||||
{/* <div
|
||||
onClick={() => {
|
||||
if (node.data && platform?.openLink) {
|
||||
platform.openLink(node.data.data_path);
|
||||
@@ -72,10 +68,29 @@ export const Component = () => {
|
||||
<Database className="mr-1 mt-[-2px] inline h-4 w-4" /> Data Folder
|
||||
</b>
|
||||
<span className="select-text">{node.data?.data_path}</span>
|
||||
</div> */}
|
||||
|
||||
<div>
|
||||
<NodeSettingLabel>Data Folder</NodeSettingLabel>
|
||||
<div className="mt-2 flex w-full flex-row gap-2">
|
||||
<Input className="grow" value={node.data?.data_path} />
|
||||
<Button size="sm" variant="outline">
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input value={node.data?.data_path + '/logs'} />
|
||||
|
||||
{/* <div className='mb-1'>
|
||||
<Label className="text-sm font-medium text-ink-faint">
|
||||
<Database className="mr-1 mt-[-2px] inline h-4 w-4" /> Logs Folder
|
||||
</Label>
|
||||
<Input value={node.data?.data_path + '/logs'} />
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="pointer-events-none mt-5 flex items-center space-x-3 opacity-50">
|
||||
<Switch size="sm" />
|
||||
<span className="text-sm font-medium text-ink-dull">
|
||||
Run Spacedrive in the background when app closed
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -86,6 +101,7 @@ export const Component = () => {
|
||||
description="Enable extra debugging features within the app."
|
||||
>
|
||||
<Switch
|
||||
size="md"
|
||||
checked={debugState.enabled}
|
||||
onClick={() => (getDebugState().enabled = !debugState.enabled)}
|
||||
/>
|
||||
|
||||
@@ -18,6 +18,7 @@ export const Component = () => {
|
||||
checked={shareTelemetry}
|
||||
onClick={() => (telemetryStore.shareTelemetry = !shareTelemetry)}
|
||||
className="m-2 ml-4"
|
||||
size="md"
|
||||
/>
|
||||
</Setting>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MaybeUndefined, useBridgeMutation, useLibraryContext } from '@sd/client';
|
||||
import { Button, Input, dialogManager } from '@sd/ui';
|
||||
import { Button, Input, Switch, Tooltip, dialogManager } from '@sd/ui';
|
||||
import { useZodForm, z } from '@sd/ui/src/forms';
|
||||
import { useDebouncedFormWatch } from '~/hooks';
|
||||
import { Heading } from '../Layout';
|
||||
@@ -61,23 +61,27 @@ export const Component = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <Setting
|
||||
<Setting
|
||||
mini
|
||||
title="Encrypt Library"
|
||||
description="Enable encryption for this library, this will only encrypt the Spacedrive database, not the files themselves."
|
||||
>
|
||||
<div className="ml-3 flex items-center">
|
||||
<Switch checked={false} />
|
||||
<Tooltip label="Library encryption coming soon">
|
||||
<Switch disabled size="md" checked={false} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Setting> */}
|
||||
</Setting>
|
||||
|
||||
{/* <Setting mini title="Export Library" description="Export this library to a file.">
|
||||
<Setting mini title="Export Library" description="Export this library to a file.">
|
||||
<div className="mt-2">
|
||||
<Button size="sm" variant="gray">
|
||||
Export
|
||||
</Button>
|
||||
<Tooltip label="Export Library coming soon">
|
||||
<Button disabled size="sm" variant="gray">
|
||||
Export
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Setting> */}
|
||||
</Setting>
|
||||
|
||||
<Setting
|
||||
mini
|
||||
|
||||
@@ -3,6 +3,8 @@ export * from './Codeblock';
|
||||
export * from './ColorPicker';
|
||||
export * from './DismissibleNotice';
|
||||
export * from './DragRegion';
|
||||
export * from './Folder';
|
||||
export * from './GridList';
|
||||
export * from './PDFViewer';
|
||||
export * from './PasswordMeter';
|
||||
export * from './SubtleButton';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ExplorerItem } from '@sd/client';
|
||||
import { getExplorerItemData } from '~/app/$libraryId/Explorer/util';
|
||||
import { ExplorerItem, getExplorerItemData } from '@sd/client';
|
||||
import { flattenThumbnailKey, useExplorerStore } from './useExplorerStore';
|
||||
|
||||
export function useExplorerItemData(explorerItem: ExplorerItem) {
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRspcLibraryContext } from '@sd/client';
|
||||
import OptionsPanel from '~/app/$libraryId/Explorer/OptionsPanel';
|
||||
import { KeyManager } from '~/app/$libraryId/KeyManager';
|
||||
import { TOP_BAR_ICON_STYLE, ToolOption } from '~/app/$libraryId/TopBar/TopBarOptions';
|
||||
// import { KeyManager } from '../app/$libraryId/KeyManager';
|
||||
import { getExplorerStore, useExplorerStore } from './useExplorerStore';
|
||||
|
||||
export const useExplorerTopBarOptions = () => {
|
||||
@@ -83,13 +83,13 @@ export const useExplorerTopBarOptions = () => {
|
||||
const { client } = useRspcLibraryContext();
|
||||
|
||||
const explorerToolOptions: ToolOption[] = [
|
||||
// {
|
||||
// toolTipLabel: 'Key Manager',
|
||||
// icon: <Key className={TOP_BAR_ICON_STYLE} />,
|
||||
// popOverComponent: <KeyManager />,
|
||||
// individual: true,
|
||||
// showAtResolution: 'xl:flex'
|
||||
// },
|
||||
{
|
||||
toolTipLabel: 'Key Manager',
|
||||
icon: <Key className={TOP_BAR_ICON_STYLE} />,
|
||||
popOverComponent: <KeyManager />,
|
||||
individual: true,
|
||||
showAtResolution: 'xl:flex'
|
||||
},
|
||||
{
|
||||
toolTipLabel: 'Tag Assign Mode',
|
||||
icon: (
|
||||
|
||||
BIN
packages/assets/icons/Keys.png
Normal file
BIN
packages/assets/icons/Keys.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -42,6 +42,7 @@ import Heart from './Heart.png';
|
||||
import HeartFlat from './HeartFlat.png';
|
||||
import Image from './Image.png';
|
||||
import Image_Light from './Image_Light.png';
|
||||
import Keys from './Keys.png';
|
||||
import Laptop from './Laptop.png';
|
||||
import Mesh from './Mesh.png';
|
||||
import Mesh_Light from './Mesh_Light.png';
|
||||
@@ -108,6 +109,7 @@ export {
|
||||
HeartFlat,
|
||||
Image,
|
||||
Image_Light,
|
||||
Keys,
|
||||
Laptop,
|
||||
Mesh,
|
||||
Mesh_Light,
|
||||
|
||||
@@ -18,6 +18,7 @@ export type Procedures = {
|
||||
{ key: "locations.indexer_rules.listForLocation", input: LibraryArgs<number>, result: IndexerRule[] } |
|
||||
{ key: "locations.list", input: LibraryArgs<null>, result: { id: number; pub_id: number[]; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; node_id: number | null; node: Node | null }[] } |
|
||||
{ key: "nodeState", input: never, result: NodeState } |
|
||||
{ key: "nodes.listLocations", input: LibraryArgs<string | null>, result: ExplorerItem[] } |
|
||||
{ key: "search.objects", input: LibraryArgs<ObjectSearchArgs>, result: SearchData<ExplorerItem> } |
|
||||
{ key: "search.paths", input: LibraryArgs<FilePathSearchArgs>, result: SearchData<ExplorerItem> } |
|
||||
{ key: "sync.messages", input: LibraryArgs<null>, result: CRDTOperation[] } |
|
||||
@@ -93,7 +94,7 @@ export type DiskType = "SSD" | "HDD" | "Removable"
|
||||
|
||||
export type EditLibraryArgs = { id: string; name: string | null; description: MaybeUndefined<string> }
|
||||
|
||||
export type ExplorerItem = { type: "Path"; has_local_thumbnail: boolean; thumbnail_key: string[] | null; item: FilePathWithObject } | { type: "Object"; has_local_thumbnail: boolean; thumbnail_key: string[] | null; item: ObjectWithFilePaths }
|
||||
export type ExplorerItem = { type: "Path"; has_local_thumbnail: boolean; thumbnail_key: string[] | null; item: FilePathWithObject } | { type: "Object"; has_local_thumbnail: boolean; thumbnail_key: string[] | null; item: ObjectWithFilePaths } | { type: "Location"; has_local_thumbnail: boolean; thumbnail_key: string[] | null; item: Location }
|
||||
|
||||
export type FileCopierJobInit = { source_location_id: number; target_location_id: number; sources_file_path_ids: number[]; target_location_relative_directory_path: string; target_file_name_suffix: string | null }
|
||||
|
||||
@@ -103,7 +104,7 @@ export type FileDeleterJobInit = { location_id: number; file_path_ids: number[]
|
||||
|
||||
export type FileEraserJobInit = { location_id: number; file_path_ids: number[]; passes: string }
|
||||
|
||||
export type FilePath = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; size_in_bytes: string | null; inode: number[] | null; device: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null }
|
||||
export type FilePath = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; device: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null }
|
||||
|
||||
export type FilePathFilterArgs = { locationId?: number | null; search?: string | null; extension?: string | null; createdAt?: OptionalRange<string>; path?: string | null; object?: ObjectFilterArgs | null }
|
||||
|
||||
@@ -111,7 +112,7 @@ export type FilePathSearchArgs = { take?: number | null; order?: FilePathSearchO
|
||||
|
||||
export type FilePathSearchOrdering = { name: SortOrder } | { sizeInBytes: SortOrder } | { dateCreated: SortOrder } | { dateModified: SortOrder } | { dateIndexed: SortOrder } | { object: ObjectSearchOrdering }
|
||||
|
||||
export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; size_in_bytes: string | null; inode: number[] | null; device: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null; object: Object | null }
|
||||
export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; size_in_bytes: string | null; size_in_bytes_bytes: number[] | null; inode: number[] | null; device: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null; object: Object | null }
|
||||
|
||||
export type FromPattern = { pattern: string; replace_all: boolean }
|
||||
|
||||
|
||||
33
packages/client/src/utils/explorerItem.ts
Normal file
33
packages/client/src/utils/explorerItem.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ExplorerItem } from '../core';
|
||||
import { ObjectKind, ObjectKindKey } from './objectKind';
|
||||
|
||||
export function getItemObject(data: ExplorerItem) {
|
||||
return data.type === 'Object' ? data.item : data.type === 'Path' ? data.item.object : null;
|
||||
}
|
||||
|
||||
export function getItemFilePath(data: ExplorerItem) {
|
||||
return data.type === 'Path'
|
||||
? data.item
|
||||
: data.type === 'Object'
|
||||
? data.item.file_paths[0]
|
||||
: null;
|
||||
}
|
||||
|
||||
export function getItemLocation(data: ExplorerItem) {
|
||||
return data.type === 'Location' ? data.item : null;
|
||||
}
|
||||
|
||||
export function getExplorerItemData(data: ExplorerItem) {
|
||||
const filePath = getItemFilePath(data);
|
||||
const objectData = getItemObject(data);
|
||||
|
||||
return {
|
||||
kind: (ObjectKind[objectData?.kind ?? 0] as ObjectKindKey) || null,
|
||||
casId: filePath?.cas_id || null,
|
||||
isDir: getItemFilePath(data)?.is_dir || false,
|
||||
extension: filePath?.extension || null,
|
||||
locationId: filePath?.location_id || null,
|
||||
hasLocalThumbnail: data.has_local_thumbnail, // this will be overwritten if new thumbnail is generated
|
||||
thumbnailKey: data.thumbnail_key
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,3 @@
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
export function bytesToNumber(bytes: number[]) {
|
||||
return bytes.reduce((acc, curr, i) => acc + curr * Math.pow(256, bytes.length - i - 1), 0);
|
||||
}
|
||||
|
||||
@@ -2,16 +2,13 @@ import { ExplorerItem } from '../core';
|
||||
|
||||
export * from './objectKind';
|
||||
export * from './formatBytes';
|
||||
export * from './explorerItem';
|
||||
// export * from './keys';
|
||||
|
||||
export function isPath(item: ExplorerItem): item is Extract<ExplorerItem, { type: 'Path' }> {
|
||||
return item.type === 'Path';
|
||||
}
|
||||
|
||||
export function isObject(item: ExplorerItem): item is Extract<ExplorerItem, { type: 'Object' }> {
|
||||
return item.type === 'Object';
|
||||
}
|
||||
|
||||
export function arraysEqual<T>(a: T[], b: T[]) {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
|
||||
@@ -23,7 +23,7 @@ const switchStyles = cva(
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'lg'
|
||||
size: 'md'
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -38,7 +38,7 @@ const thumbStyles = cva(
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'lg'
|
||||
size: 'md'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
export interface TooltipProps {
|
||||
label: string;
|
||||
position?: 'top' | 'right' | 'bottom' | 'left';
|
||||
className?: string;
|
||||
tooltipClassName?: string;
|
||||
}
|
||||
|
||||
export const Tooltip = ({
|
||||
children,
|
||||
label,
|
||||
position = 'bottom',
|
||||
className
|
||||
className,
|
||||
tooltipClassName
|
||||
}: PropsWithChildren<TooltipProps>) => {
|
||||
return (
|
||||
<TooltipPrimitive.Provider>
|
||||
@@ -22,7 +25,10 @@ export const Tooltip = ({
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
side={position}
|
||||
className="z-50 mb-[2px] max-w-[200px] rounded bg-app-darkBox px-2 py-1 text-center text-xs text-ink"
|
||||
className={clsx(
|
||||
'z-50 mb-[2px] max-w-[200px] rounded bg-app-darkBox px-2 py-1 text-center text-xs text-ink',
|
||||
tooltipClassName
|
||||
)}
|
||||
>
|
||||
<TooltipPrimitive.Arrow className="fill-app-darkBox" />
|
||||
{label}
|
||||
|
||||
Reference in New Issue
Block a user