[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:
Jamie Pine
2023-06-21 23:34:45 -07:00
committed by GitHub
parent f92ba1b57f
commit e8d3ad6005
51 changed files with 880 additions and 552 deletions

View File

@@ -41,6 +41,7 @@ poonen
rauch
ravikant
Recents
Renamable
richelsen
rspc
rspcws

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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`}>
{' '}

View File

@@ -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 && (

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "file_path" ADD COLUMN "size_in_bytes_bytes" BLOB;

View File

@@ -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

View File

@@ -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", {

View File

@@ -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![])
})
})
})
}

View File

@@ -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),

View File

@@ -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),
}

View File

@@ -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())),
]

View File

@@ -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();

View File

@@ -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();

View File

@@ -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']);

View File

@@ -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} />;
};

View File

@@ -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

View File

@@ -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} />

View File

@@ -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)

View File

@@ -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]

View 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 />;
}

View File

@@ -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({

View File

@@ -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)

View File

@@ -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>
);
}

View File

@@ -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 />}>

View File

@@ -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

View File

@@ -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') }
];

View File

@@ -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 && (

View 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} />}
</>
);
};

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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]}
/>
);
})}

View File

@@ -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';

View File

@@ -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

View File

@@ -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>
</>
);

View File

@@ -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)}
/>

View File

@@ -18,6 +18,7 @@ export const Component = () => {
checked={shareTelemetry}
onClick={() => (telemetryStore.shareTelemetry = !shareTelemetry)}
className="m-2 ml-4"
size="md"
/>
</Setting>
</>

View File

@@ -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

View File

@@ -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';

View File

@@ -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) {

View File

@@ -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: (

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -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,

View File

@@ -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 }

View 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
};
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -23,7 +23,7 @@ const switchStyles = cva(
}
},
defaultVariants: {
size: 'lg'
size: 'md'
}
}
);
@@ -38,7 +38,7 @@ const thumbStyles = cva(
}
},
defaultVariants: {
size: 'lg'
size: 'md'
}
}
);

View File

@@ -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}