[ENG-944] Sidebar UX Improvements (#1270)
* - added local section to sidebar - added spacedrop screen, showing local peers - added placeholder network screen -removed unused swift package - created a watcher for system volumes to invalidate ui when drives are added/removed * clouds * fix more imports * see more * open location if volume is location * gen assets * remove log * [ENG-939, ENG-1173] PDF Thumbnails (#1242) * sd-pdf * Process PDF blocking render inside a spawn_blocking - Load a single global Pdfium instance * Migrate pdf thumb logic to sd-images - Replace block_in_place with spawn_blocking - Only load LibHeif once - Allow thumbnailer (both indexed and non-indexed locations) to process documents - Disable loading pdf viewer in Inspection in favour of loading it's thumbnail * Try to load pdfium lib from absolute path * Revert removed import due to rebase * Small nitpick and some warnings --------- Co-authored-by: Ericson Fogo Soares <ericson.ds999@gmail.com> * [ENG-888] Media view should show current folder downward (#1437) * Done but ugly * layout * Now with a select --------- Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com> * add cool folder thing to inspector + stuff * fix text color * fix lock * fix typescript * fix ts --------- Co-authored-by: Utku Bakir <74243531+utkubakir@users.noreply.github.com> Co-authored-by: Vítor Vasconcellos <vasconcellos.dev@gmail.com> Co-authored-by: Ericson Fogo Soares <ericson.ds999@gmail.com> Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com> Co-authored-by: Brendan Allan <brendonovich@outlook.com>
BIN
Cargo.lock
generated
@@ -13,7 +13,8 @@ pub enum AppThemeType {
|
||||
swift!(pub fn lock_app_theme(theme_type: Int));
|
||||
swift!(pub fn blur_window_background(window: &NSObject));
|
||||
swift!(pub fn set_titlebar_style(window: &NSObject, transparent: Bool, large: Bool));
|
||||
|
||||
// swift!(pub fn setup_disk_watcher(window: &NSObject, transparent: Bool, large: Bool));
|
||||
// swift!(pub fn disk_event_callback(mounted: Bool, path: &SRString));
|
||||
swift!(pub fn reload_webview(webview: &NSObject));
|
||||
|
||||
#[repr(C)]
|
||||
@@ -31,3 +32,20 @@ pub fn open_file_paths_with(file_urls: &[String], with_url: &str) {
|
||||
let file_url = file_urls.join("\0");
|
||||
unsafe { open_file_path_with(&file_url.as_str().into(), &with_url.into()) }
|
||||
}
|
||||
|
||||
// main!(|_| {
|
||||
// unsafe { setup_disk_watcher() };
|
||||
// print!("Waiting for disk events... ");
|
||||
// Ok(())
|
||||
// });
|
||||
|
||||
// #[no_mangle]
|
||||
// pub extern "C" fn disk_event_callback(mounted: Bool, path: *const SRString) {
|
||||
// let mounted_str = if mounted { "mounted" } else { "unmounted" };
|
||||
|
||||
// // Convert the raw pointer to a reference
|
||||
// let path_ref = unsafe { &*path };
|
||||
// let path_str = path_ref.to_string(); // Assuming SRString has a to_string method
|
||||
|
||||
// println!("Disk at path {} was {}", path_str, mounted_str);
|
||||
// }
|
||||
|
||||
@@ -67,7 +67,11 @@ const Explorer = ({ items }: ExplorerProps) => {
|
||||
numColumns={layoutMode === 'grid' ? getExplorerStore().gridNumColumns : 1}
|
||||
data={items}
|
||||
keyExtractor={(item) =>
|
||||
item.type === 'NonIndexedPath' ? item.item.path : item.item.id.toString()
|
||||
item.type === 'NonIndexedPath'
|
||||
? item.item.path
|
||||
: item.type === 'SpacedropPeer'
|
||||
? item.item.name
|
||||
: item.item.id.toString()
|
||||
}
|
||||
renderItem={({ item }) => (
|
||||
<Pressable onPress={() => handlePress(item)}>
|
||||
|
||||
@@ -107,11 +107,13 @@ const FileInfoModal = forwardRef<ModalRef, FileInfoModalProps>((props, ref) => {
|
||||
/>
|
||||
)} */}
|
||||
{/* Created */}
|
||||
<MetaItem
|
||||
icon={Clock}
|
||||
title="Created"
|
||||
value={dayjs(item?.date_created).format('MMM Do YYYY')}
|
||||
/>
|
||||
{data.type !== 'SpacedropPeer' && (
|
||||
<MetaItem
|
||||
icon={Clock}
|
||||
title="Created"
|
||||
value={dayjs(data.item.date_created).format('MMM Do YYYY')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{filePathData && 'cas_id' in filePathData && (
|
||||
<>
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
scan_location, scan_location_sub_path, LocationCreateArgs, LocationError,
|
||||
LocationUpdateArgs,
|
||||
},
|
||||
p2p::PeerMetadata,
|
||||
prisma::{file_path, indexer_rule, indexer_rules_in_location, location, object, SortOrder},
|
||||
util::AbortOnDrop,
|
||||
};
|
||||
@@ -46,6 +47,11 @@ pub enum ExplorerItem {
|
||||
thumbnail_key: Option<Vec<String>>,
|
||||
item: NonIndexedPathItem,
|
||||
},
|
||||
SpacedropPeer {
|
||||
has_local_thumbnail: bool,
|
||||
thumbnail_key: Option<Vec<String>>,
|
||||
item: PeerMetadata,
|
||||
},
|
||||
}
|
||||
|
||||
impl ExplorerItem {
|
||||
@@ -117,6 +123,7 @@ impl ExplorerItem {
|
||||
} => date_created.map(Into::into).unwrap_or_default(),
|
||||
|
||||
ExplorerItem::NonIndexedPath { item, .. } => item.date_created,
|
||||
_ => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ impl Node {
|
||||
node.p2p.start(p2p_stream, node.clone());
|
||||
|
||||
let router = api::mount();
|
||||
|
||||
info!("Spacedrive online.");
|
||||
Ok((node, router))
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ pub enum Category {
|
||||
Books,
|
||||
Contacts,
|
||||
Trash,
|
||||
Screenshots,
|
||||
}
|
||||
|
||||
impl Category {
|
||||
@@ -55,6 +56,7 @@ impl Category {
|
||||
Category::Databases => ObjectKind::Database,
|
||||
Category::Archives => ObjectKind::Archive,
|
||||
Category::Applications => ObjectKind::Executable,
|
||||
Category::Screenshots => ObjectKind::Screenshot,
|
||||
_ => unimplemented!("Category::to_object_kind() for {:?}", self),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
migrator::Migrate,
|
||||
mpscrr, MaybeUndefined,
|
||||
},
|
||||
volume::watcher::spawn_volume_watcher,
|
||||
Node,
|
||||
};
|
||||
|
||||
@@ -122,8 +123,11 @@ impl Libraries {
|
||||
Err(e) => return Err(FileIOError::from((db_path, e)).into()),
|
||||
}
|
||||
|
||||
self.load(library_id, &db_path, config_path, None, true, node)
|
||||
let library_arc = self
|
||||
.load(library_id, &db_path, config_path, None, true, node)
|
||||
.await?;
|
||||
|
||||
spawn_volume_watcher(library_arc.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
|
||||
pub mod watcher;
|
||||
|
||||
fn sys_guard() -> &'static Mutex<System> {
|
||||
static SYS: OnceLock<Mutex<System>> = OnceLock::new();
|
||||
SYS.get_or_init(|| Mutex::new(System::new_all()))
|
||||
|
||||
81
core/src/volume/watcher.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use std::io::BufRead;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::invalidate_query;
|
||||
use crate::library::Library;
|
||||
|
||||
/// Currently the only thing we do is invalidate the volumes.list query.
|
||||
/// Later, we will want to extract specific data into a struct.
|
||||
/// That way we can determine if we want to trigger the import files flow.
|
||||
///
|
||||
fn handle_disk_change(library: Arc<Library>) {
|
||||
// Clone the Arc to be moved into the closure
|
||||
let library_cloned = library.clone();
|
||||
|
||||
// Spawn a new thread to perform a delayed operation
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(500)); // Delay for 500 milliseconds
|
||||
invalidate_query!(library_cloned, "volumes.list");
|
||||
});
|
||||
}
|
||||
|
||||
pub fn spawn_volume_watcher(library: Arc<Library>) {
|
||||
#[cfg(target_os = "macos")]
|
||||
thread::spawn(move || {
|
||||
let mut child = Command::new("diskutil")
|
||||
.arg("activity")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.expect("Failed to start diskutil");
|
||||
|
||||
let stdout = child.stdout.as_mut().expect("Failed to capture stdout");
|
||||
let mut reader = std::io::BufReader::new(stdout);
|
||||
|
||||
let mut buffer = String::new();
|
||||
while reader.read_line(&mut buffer).expect("Failed to read line") > 0 {
|
||||
if buffer.contains("DiskAppeared") || buffer.contains("DiskDisappeared") {
|
||||
// println!("Disk change detected: {:?}", &buffer);
|
||||
handle_disk_change(library.clone());
|
||||
}
|
||||
buffer.clear();
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
thread::spawn(move || {
|
||||
let mut child = Command::new("udevadm")
|
||||
.arg("monitor")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.expect("Failed to start udevadm");
|
||||
|
||||
let stdout = child.stdout.as_mut().expect("Failed to capture stdout");
|
||||
let mut reader = std::io::BufReader::new(stdout);
|
||||
|
||||
let mut buffer = String::new();
|
||||
while reader.read_line(&mut buffer).expect("Failed to read line") > 0 {
|
||||
if buffer.contains("add") || buffer.contains("remove") {
|
||||
println!("Disk change detected: {:?}", &buffer);
|
||||
handle_disk_change(library.clone());
|
||||
}
|
||||
|
||||
buffer.clear();
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
thread::spawn(move || {
|
||||
let mut child = Command::new("wmic")
|
||||
.arg("diskdrive")
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.expect("Failed to start wmic");
|
||||
|
||||
// Shared handling code
|
||||
// ...
|
||||
// handle_disk_change(library.clone());
|
||||
});
|
||||
}
|
||||
@@ -52,4 +52,8 @@ pub enum ObjectKind {
|
||||
Book = 22,
|
||||
/// Config file
|
||||
Config = 23,
|
||||
/// Dotfile
|
||||
Dotfile = 24,
|
||||
/// Screenshot
|
||||
Screenshot = 25,
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "sd-macos"
|
||||
version = "0.1.0"
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
swift-rs = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
swift-rs = { workspace = true, features = ["build"] }
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "SwiftRs",
|
||||
"repositoryURL": "https://github.com/brendonovich/swift-rs",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "cbb9b96b6036108e76879713e910c05bc9e145c7",
|
||||
"version": "1.0.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "sd-macos",
|
||||
platforms: [
|
||||
.macOS(.v11),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "sd-macos",
|
||||
type: .static,
|
||||
targets: ["sd-macos"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
.package(url: "https://github.com/brendonovich/swift-rs", from: "1.0.1"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "sd-macos",
|
||||
dependencies: [.product(name: "SwiftRs", package: "swift-rs")],
|
||||
path: "src-swift")
|
||||
]
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let deployment_target =
|
||||
env::var("MACOSX_DEPLOYMENT_TARGET").unwrap_or_else(|_| String::from("10.15"));
|
||||
|
||||
swift_rs::SwiftLinker::new(deployment_target.as_str())
|
||||
.with_package("sd-macos", "./")
|
||||
.link()
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
import SwiftRs
|
||||
|
||||
@_cdecl("get_file_thumbnail_base64")
|
||||
public func getFileThumbnailBase64(path: SRString) -> SRString {
|
||||
let path = path.toString();
|
||||
|
||||
let image = NSWorkspace.shared.icon(forFile: path)
|
||||
let bitmap = NSBitmapImageRep(data: image.tiffRepresentation!)!.representation(using: .png, properties: [:])!
|
||||
|
||||
return SRString(bitmap.base64EncodedString())
|
||||
}
|
||||
|
||||
class Volume: NSObject {
|
||||
var name: SRString
|
||||
var path: SRString
|
||||
var total_capacity: Int
|
||||
var available_capacity: Int
|
||||
var is_removable: Bool
|
||||
var is_ejectable: Bool
|
||||
var is_root_filesystem: Bool
|
||||
|
||||
internal init(name: String, path: String, total_capacity: Int, available_capacity: Int, is_removable: Bool, is_ejectable: Bool, is_root_filesystem: Bool) {
|
||||
self.name = SRString(name)
|
||||
self.path = SRString(path)
|
||||
self.total_capacity = total_capacity
|
||||
self.available_capacity = available_capacity
|
||||
self.is_removable = is_removable
|
||||
self.is_ejectable = is_ejectable
|
||||
self.is_root_filesystem = is_root_filesystem
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("get_mounts")
|
||||
public func getMounts() -> SRObjectArray {
|
||||
let keys: [URLResourceKey] = [
|
||||
.volumeNameKey,
|
||||
.volumeIsRemovableKey,
|
||||
.volumeIsEjectableKey,
|
||||
.volumeTotalCapacityKey,
|
||||
.volumeAvailableCapacityKey,
|
||||
.volumeIsRootFileSystemKey,
|
||||
]
|
||||
let paths = FileManager().mountedVolumeURLs(includingResourceValuesForKeys: keys, options: [])
|
||||
|
||||
var validMounts: [Volume] = []
|
||||
|
||||
if let urls = paths {
|
||||
for url in urls {
|
||||
let components = url.pathComponents
|
||||
if components.count == 1 || components.count > 1
|
||||
&& components[1] == "Volumes"
|
||||
{
|
||||
let metadata = try? url.promisedItemResourceValues(forKeys: Set(keys))
|
||||
|
||||
let volume = Volume(
|
||||
name: metadata?.volumeName ?? "",
|
||||
path: url.path,
|
||||
total_capacity: metadata?.volumeTotalCapacity ?? 0,
|
||||
available_capacity: metadata?.volumeAvailableCapacity ?? 0,
|
||||
is_removable: metadata?.volumeIsRemovable ?? false,
|
||||
is_ejectable: metadata?.volumeIsEjectable ?? false,
|
||||
is_root_filesystem: metadata?.volumeIsRootFileSystem ?? false
|
||||
)
|
||||
|
||||
validMounts.append(volume)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SRObjectArray(validMounts)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#![cfg(target_os = "macos")]
|
||||
|
||||
use swift_rs::*;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Volume {
|
||||
name: SRString,
|
||||
path: SRString,
|
||||
total_capacity: Int,
|
||||
available_capacity: Int,
|
||||
is_removable: Bool,
|
||||
is_ejectable: Bool,
|
||||
is_root_filesystem: Bool,
|
||||
}
|
||||
|
||||
swift!(pub fn get_file_thumbnail_base64(name: &SRString) -> SRString);
|
||||
swift!(pub fn get_mounts() -> SRObjectArray<Volume>);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getIcon, iconNames } from '@sd/assets/util';
|
||||
import { getIcon, getIconByName, iconNames } from '@sd/assets/util';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
memo,
|
||||
@@ -120,9 +120,14 @@ export const FileThumb = memo((props: ThumbProps) => {
|
||||
break;
|
||||
|
||||
default:
|
||||
if (itemData.customIcon) {
|
||||
setSrc(getIconByName(itemData.customIcon as any));
|
||||
break;
|
||||
}
|
||||
setSrc(
|
||||
getIcon(
|
||||
itemData.isDir || parent?.type === 'Node' ? 'Folder' : itemData.kind,
|
||||
// itemData.isDir || parent?.type === 'Node' ? 'Folder' :
|
||||
itemData.kind,
|
||||
isDark,
|
||||
itemData.extension,
|
||||
itemData.isDir
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Barcode,
|
||||
BookOpenText,
|
||||
CircleWavyCheck,
|
||||
Clock,
|
||||
Cube,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import { Link as NavLink } from 'react-router-dom';
|
||||
import Sticky from 'react-sticky-el';
|
||||
import {
|
||||
byteSize,
|
||||
FilePath,
|
||||
@@ -47,6 +49,7 @@ import AssignTagMenuItems from '~/components/AssignTagMenuItems';
|
||||
import { useIsDark, useZodRouteParams } from '~/hooks';
|
||||
import { isNonEmpty } from '~/util';
|
||||
|
||||
import { Folder } from '../../../../components';
|
||||
import { useExplorerContext } from '../Context';
|
||||
import { FileThumb } from '../FilePath/Thumb';
|
||||
import { useQuickPreviewStore } from '../QuickPreview/store';
|
||||
@@ -100,27 +103,33 @@ export const Inspector = forwardRef<HTMLDivElement, Props>(
|
||||
|
||||
return (
|
||||
<div ref={ref} style={{ width: INSPECTOR_WIDTH, ...style }} {...props}>
|
||||
{showThumbnail && (
|
||||
<div className="relative mb-2 flex aspect-square items-center justify-center px-2">
|
||||
{isNonEmpty(selectedItems) ? (
|
||||
<Thumbnails items={selectedItems} />
|
||||
<Sticky
|
||||
scrollElement={explorer.scrollRef.current || undefined}
|
||||
stickyClassName="!top-[40px]"
|
||||
topOffset={-40}
|
||||
>
|
||||
{showThumbnail && (
|
||||
<div className="relative mb-2 flex aspect-square items-center justify-center px-2">
|
||||
{isNonEmpty(selectedItems) ? (
|
||||
<Thumbnails items={selectedItems} />
|
||||
) : (
|
||||
<img src={isDark ? Image : Image_Light} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex select-text flex-col overflow-hidden rounded-lg border border-app-line bg-app-box py-0.5 shadow-app-shade/10">
|
||||
{!isNonEmpty(selectedItems) ? (
|
||||
<div className="flex h-[390px] items-center justify-center text-sm text-ink-dull">
|
||||
Nothing selected
|
||||
</div>
|
||||
) : selectedItems.length === 1 ? (
|
||||
<SingleItemMetadata item={selectedItems[0]} />
|
||||
) : (
|
||||
<img src={isDark ? Image : Image_Light} />
|
||||
<MultiItemMetadata items={selectedItems} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex select-text flex-col overflow-hidden rounded-lg border border-app-line bg-app-box py-0.5 shadow-app-shade/10">
|
||||
{!isNonEmpty(selectedItems) ? (
|
||||
<div className="flex h-[390px] items-center justify-center text-sm text-ink-dull">
|
||||
Nothing selected
|
||||
</div>
|
||||
) : selectedItems.length === 1 ? (
|
||||
<SingleItemMetadata item={selectedItems[0]} />
|
||||
) : (
|
||||
<MultiItemMetadata items={selectedItems} />
|
||||
)}
|
||||
</div>
|
||||
</Sticky>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -166,6 +175,8 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
|
||||
let filePathData: FilePath | FilePathWithObject | null = null;
|
||||
let ephemeralPathData: NonIndexedPathItem | null = null;
|
||||
|
||||
const locations = useLibraryQuery(['locations.list']);
|
||||
|
||||
switch (item.type) {
|
||||
case 'NonIndexedPath': {
|
||||
ephemeralPathData = item.item;
|
||||
@@ -181,8 +192,28 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
|
||||
filePathData = item.item.file_paths[0] ?? null;
|
||||
break;
|
||||
}
|
||||
case 'SpacedropPeer': {
|
||||
objectData = item.item as unknown as Object;
|
||||
// filePathData = item.item.file_paths[0] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueLocationIds = useMemo(() => {
|
||||
return item.type === 'Object'
|
||||
? [
|
||||
...new Set(
|
||||
(item.item?.file_paths || []).map((fp) => fp.location_id).filter(Boolean)
|
||||
)
|
||||
]
|
||||
: item.type === 'Path'
|
||||
? [item.item.location_id]
|
||||
: [];
|
||||
}, [item]);
|
||||
|
||||
const fileLocations =
|
||||
locations.data?.filter((location) => uniqueLocationIds.includes(location.id)) || [];
|
||||
|
||||
const readyToFetch = useIsFetchReady(item);
|
||||
const tags = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1], {
|
||||
enabled: objectData != null && readyToFetch
|
||||
@@ -279,6 +310,23 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
|
||||
/>
|
||||
</MetaContainer>
|
||||
|
||||
{fileLocations.length > 0 && (
|
||||
<MetaContainer>
|
||||
<MetaTitle>Locations</MetaTitle>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{fileLocations.map((location) => (
|
||||
<div
|
||||
className="flex flex-row rounded bg-app-hover/60 px-1 py-0.5 hover:bg-app-selected"
|
||||
key={location.id}
|
||||
>
|
||||
<Folder size={18} />
|
||||
<span className="ml-1 text-xs text-ink">{location.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MetaContainer>
|
||||
)}
|
||||
|
||||
{mediaData.data && <MediaData data={mediaData.data} />}
|
||||
|
||||
<MetaContainer className="flex !flex-row flex-wrap gap-1 overflow-hidden">
|
||||
|
||||
@@ -96,7 +96,11 @@ export const useTable = () => {
|
||||
{
|
||||
id: 'dateCreated',
|
||||
header: 'Date Created',
|
||||
accessorFn: (file) => dayjs(file.item.date_created).format('MMM Do YYYY')
|
||||
accessorFn: (file) => {
|
||||
if (file.type === 'SpacedropPeer') return null;
|
||||
|
||||
dayjs(file.item.date_created).format('MMM Do YYYY');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'dateModified',
|
||||
|
||||
@@ -23,7 +23,14 @@ export default function RenamableItemText(props: {
|
||||
disabled: !selected || disabled
|
||||
};
|
||||
|
||||
if (item.type === 'Location') {
|
||||
if (item.type === 'SpacedropPeer') {
|
||||
// TODO: do this better, I just copied the styles to a new span, bad
|
||||
return (
|
||||
<span className="cursor-default truncate rounded-md px-1.5 py-px text-xs text-ink">
|
||||
{item.item.name}
|
||||
</span>
|
||||
);
|
||||
} else if (item.type === 'Location') {
|
||||
const locationData = item.item;
|
||||
return (
|
||||
<RenameLocationTextBox
|
||||
|
||||
@@ -48,10 +48,16 @@ export const useViewItemDoubleClick = () => {
|
||||
items.non_indexed.splice(sameAsClicked ? 0 : -1, 0, selectedItem.item);
|
||||
break;
|
||||
}
|
||||
case 'SpacedropPeer': {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (const filePath of selectedItem.type === 'Path'
|
||||
? [selectedItem.item]
|
||||
: selectedItem.item.file_paths) {
|
||||
const paths =
|
||||
selectedItem.type === 'Path'
|
||||
? [selectedItem.item]
|
||||
: selectedItem.item.file_paths;
|
||||
|
||||
for (const filePath of paths) {
|
||||
if (isPath(selectedItem) && selectedItem.item.is_dir) {
|
||||
items.dirs.splice(sameAsClicked ? 0 : -1, 0, filePath);
|
||||
} else {
|
||||
|
||||
@@ -151,7 +151,7 @@ export function getExplorerStore() {
|
||||
}
|
||||
|
||||
export function isCut(item: ExplorerItem, cutCopyState: ReadonlyDeep<CutCopyState>) {
|
||||
return item.type === 'NonIndexedPath'
|
||||
return item.type === 'NonIndexedPath' || item.type === 'SpacedropPeer'
|
||||
? false
|
||||
: cutCopyState.type === 'Cut' && cutCopyState.sourcePathIds.includes(item.item.id);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ export const uniqueId = (item: ExplorerItem | { pub_id: number[] }) => {
|
||||
switch (type) {
|
||||
case 'NonIndexedPath':
|
||||
return item.item.path;
|
||||
case 'SpacedropPeer':
|
||||
return item.item.name;
|
||||
default:
|
||||
return pubIdToString(item.item.pub_id);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import {
|
||||
ArrowsClockwise,
|
||||
CopySimple,
|
||||
Crosshair,
|
||||
Eraser,
|
||||
FilmStrip,
|
||||
Planet
|
||||
} from '@phosphor-icons/react';
|
||||
import { ArrowsClockwise, Broadcast, Planet } from '@phosphor-icons/react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useKeys } from 'rooks';
|
||||
import { LibraryContextProvider, useClientContext, useFeatureFlag } from '@sd/client';
|
||||
import { modifierSymbols, Tooltip } from '@sd/ui';
|
||||
import { SubtleButton } from '~/components/SubtleButton';
|
||||
import { useKeyMatcher } from '~/hooks';
|
||||
|
||||
import { EphemeralSection } from './EphemeralSection';
|
||||
import Icon from './Icon';
|
||||
import { LibrarySection } from './LibrarySection';
|
||||
import SidebarLink from './Link';
|
||||
import Section from './Section';
|
||||
|
||||
export default () => {
|
||||
const { library } = useClientContext();
|
||||
@@ -45,8 +36,9 @@ export default () => {
|
||||
{/* <SidebarLink to="spacedrop">
|
||||
<Icon component={Broadcast} />
|
||||
Spacedrop
|
||||
</SidebarLink>
|
||||
<SidebarLink to="imports">
|
||||
</SidebarLink> */}
|
||||
{/*
|
||||
{/* <SidebarLink to="imports">
|
||||
<Icon component={ArchiveBox} />
|
||||
Imports
|
||||
</SidebarLink> */}
|
||||
@@ -63,10 +55,10 @@ export default () => {
|
||||
<LibrarySection />
|
||||
</LibraryContextProvider>
|
||||
)}
|
||||
<Section name="Tools" actionArea={<SubtleButton />}>
|
||||
{/* <Section name="Tools" actionArea={<SubtleButton />}>
|
||||
<SidebarLink disabled to="duplicate-finder">
|
||||
<Icon component={CopySimple} />
|
||||
Duplicate Finder
|
||||
Duplicates
|
||||
</SidebarLink>
|
||||
<SidebarLink disabled to="lost-and-found">
|
||||
<Icon component={Crosshair} />
|
||||
@@ -80,7 +72,7 @@ export default () => {
|
||||
<Icon component={FilmStrip} />
|
||||
Media Encoder
|
||||
</SidebarLink>
|
||||
</Section>
|
||||
</Section> */}
|
||||
<div className="grow" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,58 +1,151 @@
|
||||
import { useState } from 'react';
|
||||
import { useBridgeQuery } from '@sd/client';
|
||||
import { Folder } from '~/components';
|
||||
import { EjectSimple } from '@phosphor-icons/react';
|
||||
import { Drive, Globe, HDD, Home, SD } from '@sd/assets/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useBridgeQuery, useLibraryQuery } from '@sd/client';
|
||||
import { Button, tw } from '@sd/ui';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
import SidebarLink from './Link';
|
||||
import Section from './Section';
|
||||
import SeeMore from './SeeMore';
|
||||
|
||||
const SidebarIcon = tw.img`mr-1 h-5 w-5`;
|
||||
const Name = tw.span`truncate`;
|
||||
|
||||
const EjectButton = ({ className }: { className?: string }) => (
|
||||
<Button className={clsx('absolute right-[2px] !p-[5px]', className)} variant="subtle">
|
||||
<EjectSimple weight="fill" size={18} className="h-3 w-3 opacity-70" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const EphemeralSection = () => {
|
||||
const [home, setHome] = useState<string | null>(null);
|
||||
|
||||
const platform = usePlatform();
|
||||
platform.userHomeDir?.().then(setHome);
|
||||
|
||||
const volumes = useBridgeQuery(['volumes.list']).data ?? [];
|
||||
const locations = useLibraryQuery(['locations.list']);
|
||||
|
||||
return home == null && volumes.length < 1 ? null : (
|
||||
const volumes = useBridgeQuery(['volumes.list']);
|
||||
|
||||
// this will return an array of location ids that are also volumes
|
||||
// { "/Mount/Point": 1, "/Mount/Point2": 2"}
|
||||
type LocationIdsMap = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
const locationIdsForVolumes = useMemo<LocationIdsMap>(() => {
|
||||
if (!locations.data || !volumes.data) return {};
|
||||
|
||||
const volumePaths = volumes.data.map((volume) => volume.mount_points[0] ?? null);
|
||||
|
||||
const matchedLocations = locations.data.filter((location) =>
|
||||
volumePaths.includes(location.path)
|
||||
);
|
||||
|
||||
const locationIdsMap = matchedLocations.reduce((acc, location) => {
|
||||
if (location.path) {
|
||||
acc[location.path] = location.id;
|
||||
}
|
||||
return acc;
|
||||
}, {} as LocationIdsMap);
|
||||
|
||||
return locationIdsMap;
|
||||
}, [locations.data, volumes.data]);
|
||||
|
||||
console.log('locationIdsForVolumes', locationIdsForVolumes);
|
||||
|
||||
const items = [
|
||||
{ type: 'network' },
|
||||
home ? { type: 'home', path: home } : null,
|
||||
...(volumes.data || []).flatMap((volume, volumeIndex) =>
|
||||
volume.mount_points.map((mountPoint, index) =>
|
||||
mountPoint !== home
|
||||
? { type: 'volume', volume, mountPoint, volumeIndex, index }
|
||||
: null
|
||||
)
|
||||
)
|
||||
].filter(Boolean) as Array<{
|
||||
type: string;
|
||||
path?: string;
|
||||
volume?: any;
|
||||
mountPoint?: string;
|
||||
volumeIndex?: number;
|
||||
index?: number;
|
||||
}>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section name="Explore">
|
||||
{home && (
|
||||
<SidebarLink
|
||||
to={`ephemeral/0?path=${home}`}
|
||||
className="group relative w-full border border-transparent"
|
||||
>
|
||||
<div className="relative -mt-0.5 mr-1 shrink-0 grow-0">
|
||||
<Folder size={18} />
|
||||
</div>
|
||||
<Section name="Local">
|
||||
<SeeMore
|
||||
items={items}
|
||||
renderItem={(item, index) => {
|
||||
const locationId = locationIdsForVolumes[item.mountPoint ?? ''];
|
||||
|
||||
<span className="truncate">Home</span>
|
||||
</SidebarLink>
|
||||
)}
|
||||
{volumes.map((volume, volumeIndex) => {
|
||||
const mountPoints = volume.mount_points;
|
||||
mountPoints.sort((a, b) => a.length - b.length);
|
||||
return mountPoints.map((mountPoint, index) => {
|
||||
const key = `${volumeIndex}-${index}`;
|
||||
if (mountPoint == home) return null;
|
||||
if (item?.type === 'network') {
|
||||
return (
|
||||
<SidebarLink
|
||||
className="group relative w-full"
|
||||
to={`network/34`}
|
||||
key={index}
|
||||
>
|
||||
<SidebarIcon src={Globe} />
|
||||
<Name>Network</Name>
|
||||
</SidebarLink>
|
||||
);
|
||||
}
|
||||
|
||||
const name =
|
||||
mountPoint === '/' ? 'Root' : index === 0 ? volume.name : mountPoint;
|
||||
return (
|
||||
<SidebarLink
|
||||
to={`ephemeral/${key}?path=${mountPoint}`}
|
||||
key={key}
|
||||
className="group relative w-full border border-transparent"
|
||||
>
|
||||
<div className="relative -mt-0.5 mr-1 shrink-0 grow-0">
|
||||
<Folder size={18} />
|
||||
</div>
|
||||
if (item?.type === 'home') {
|
||||
return (
|
||||
<SidebarLink
|
||||
to={`ephemeral/0?path=${item.path}`}
|
||||
className="group relative w-full border border-transparent"
|
||||
key={index}
|
||||
>
|
||||
<SidebarIcon src={Home} />
|
||||
<Name>Home</Name>
|
||||
</SidebarLink>
|
||||
);
|
||||
}
|
||||
|
||||
<span className="truncate">{name}</span>
|
||||
</SidebarLink>
|
||||
);
|
||||
});
|
||||
})}
|
||||
if (item?.type === 'volume') {
|
||||
const key = `${item.volumeIndex}-${item.index}`;
|
||||
const name =
|
||||
item.mountPoint === '/'
|
||||
? 'Root'
|
||||
: item.index === 0
|
||||
? item.volume.name
|
||||
: item.mountPoint;
|
||||
|
||||
const toPath =
|
||||
locationId !== undefined
|
||||
? `location/${locationId}`
|
||||
: `ephemeral/${key}?path=${item.mountPoint}`;
|
||||
|
||||
return (
|
||||
<SidebarLink
|
||||
to={toPath}
|
||||
key={key}
|
||||
className="group relative w-full border border-transparent"
|
||||
>
|
||||
<SidebarIcon
|
||||
src={
|
||||
item.volume.file_system === 'exfat'
|
||||
? SD
|
||||
: item.volume.name === 'Macintosh HD'
|
||||
? HDD
|
||||
: Drive
|
||||
}
|
||||
/>
|
||||
<Name>{name}</Name>
|
||||
{item.volume.disk_type === 'Removable' && <EjectButton />}
|
||||
</SidebarLink>
|
||||
);
|
||||
}
|
||||
|
||||
return null; // This should never be reached, but is here to satisfy TypeScript
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EjectSimple } from '@phosphor-icons/react';
|
||||
import { Laptop, Mobile, Server } from '@sd/assets/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -17,8 +18,21 @@ import { Folder, SubtleButton } from '~/components';
|
||||
import SidebarLink from './Link';
|
||||
import LocationsContextMenu from './LocationsContextMenu';
|
||||
import Section from './Section';
|
||||
import SeeMore from './SeeMore';
|
||||
import TagsContextMenu from './TagsContextMenu';
|
||||
|
||||
type SidebarGroup = {
|
||||
name: string;
|
||||
items: SidebarItem[];
|
||||
};
|
||||
|
||||
type SidebarItem = {
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
to: string;
|
||||
position: number;
|
||||
};
|
||||
|
||||
type TriggeredContextItem =
|
||||
| {
|
||||
type: 'location';
|
||||
@@ -29,7 +43,11 @@ type TriggeredContextItem =
|
||||
tagId: number;
|
||||
};
|
||||
|
||||
const SEE_MORE_LOCATIONS_COUNT = 5;
|
||||
const EjectButton = ({ className }: { className?: string }) => (
|
||||
<Button className={clsx('absolute right-[2px] !p-[5px]', className)} variant="subtle">
|
||||
<EjectSimple weight="bold" size={18} className="h-3 w-3 opacity-70" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const LibrarySection = () => {
|
||||
const debugState = useDebugState();
|
||||
@@ -44,11 +62,6 @@ export const LibrarySection = () => {
|
||||
|
||||
const [seeMoreLocations, setSeeMoreLocations] = useState(false);
|
||||
|
||||
const locations = locationsQuery.data?.slice(
|
||||
0,
|
||||
seeMoreLocations ? undefined : SEE_MORE_LOCATIONS_COUNT
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const outsideClick = () => {
|
||||
document.addEventListener('click', () => {
|
||||
@@ -64,7 +77,7 @@ export const LibrarySection = () => {
|
||||
return (
|
||||
<>
|
||||
<Section
|
||||
name="Nodes"
|
||||
name="Devices"
|
||||
actionArea={
|
||||
isPairingEnabled && (
|
||||
<Link to="settings/library/nodes">
|
||||
@@ -83,7 +96,8 @@ export const LibrarySection = () => {
|
||||
<img src={Laptop} className="mr-1 h-5 w-5" />
|
||||
<span className="truncate">{node.data.name}</span>
|
||||
</SidebarLink>
|
||||
{debugState.enabled && (
|
||||
|
||||
{/* {debugState.enabled && (
|
||||
<>
|
||||
<SidebarLink
|
||||
className="group relative w-full"
|
||||
@@ -102,7 +116,7 @@ export const LibrarySection = () => {
|
||||
<span className="truncate">Titan</span>
|
||||
</SidebarLink>
|
||||
</>
|
||||
)}
|
||||
)} */}
|
||||
</>
|
||||
)}
|
||||
<Tooltip
|
||||
@@ -111,10 +125,11 @@ export const LibrarySection = () => {
|
||||
position="right"
|
||||
>
|
||||
<Button disabled variant="dotted" className="mt-1 w-full">
|
||||
Connect Node
|
||||
Add Device
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
name="Locations"
|
||||
actionArea={
|
||||
@@ -123,10 +138,9 @@ export const LibrarySection = () => {
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
{locations?.map((location) => {
|
||||
const online = onlineLocations.some((l) => arraysEqual(location.pub_id, l));
|
||||
|
||||
return (
|
||||
<SeeMore
|
||||
items={locationsQuery.data || []}
|
||||
renderItem={(location, index) => (
|
||||
<LocationsContextMenu key={location.id} locationId={location.id}>
|
||||
<SidebarLink
|
||||
onContextMenu={() =>
|
||||
@@ -149,7 +163,11 @@ export const LibrarySection = () => {
|
||||
<div
|
||||
className={clsx(
|
||||
'absolute bottom-0.5 right-0 h-1.5 w-1.5 rounded-full',
|
||||
online ? 'bg-green-500' : 'bg-red-500'
|
||||
onlineLocations.some((l) =>
|
||||
arraysEqual(location.pub_id, l)
|
||||
)
|
||||
? 'bg-green-500'
|
||||
: 'bg-red-500'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -157,16 +175,8 @@ export const LibrarySection = () => {
|
||||
<span className="truncate">{location.name}</span>
|
||||
</SidebarLink>
|
||||
</LocationsContextMenu>
|
||||
);
|
||||
})}
|
||||
{locationsQuery.data?.[SEE_MORE_LOCATIONS_COUNT] && (
|
||||
<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 && (
|
||||
@@ -178,8 +188,9 @@ export const LibrarySection = () => {
|
||||
</NavLink>
|
||||
}
|
||||
>
|
||||
<div className="mb-2 mt-1">
|
||||
{tags.data?.slice(0, 6).map((tag) => (
|
||||
<SeeMore
|
||||
items={tags.data}
|
||||
renderItem={(tag, index) => (
|
||||
<TagsContextMenu tagId={tag.id} key={tag.id}>
|
||||
<SidebarLink
|
||||
onContextMenu={() =>
|
||||
@@ -204,8 +215,8 @@ export const LibrarySection = () => {
|
||||
<span className="ml-1.5 truncate text-sm">{tag.name}</span>
|
||||
</SidebarLink>
|
||||
</TagsContextMenu>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
</>
|
||||
|
||||
31
interface/app/$libraryId/Layout/Sidebar/SeeMore.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ReactNode, useState } from 'react';
|
||||
|
||||
export const SEE_MORE_COUNT = 5;
|
||||
|
||||
interface SeeMoreProps<T> {
|
||||
items: T[];
|
||||
renderItem: (item: T, index: number) => ReactNode;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
const SeeMore = <T,>({ items, renderItem, limit = SEE_MORE_COUNT }: SeeMoreProps<T>) => {
|
||||
const [seeMore, setSeeMore] = useState(false);
|
||||
|
||||
const displayedItems = seeMore ? items : items.slice(0, limit);
|
||||
|
||||
return (
|
||||
<>
|
||||
{displayedItems.map((item, index) => renderItem(item, index))}
|
||||
{items.length > limit && (
|
||||
<div
|
||||
onClick={() => setSeeMore(!seeMore)}
|
||||
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 {seeMore ? 'less' : 'more'}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeeMore;
|
||||
@@ -78,7 +78,7 @@ export default () => {
|
||||
<Input
|
||||
ref={searchRef}
|
||||
placeholder="Search"
|
||||
className="w-52 transition-all duration-200 focus-within:w-60"
|
||||
className="mx-2 w-48 transition-all duration-200 focus-within:w-60"
|
||||
size="sm"
|
||||
onChange={(e) => updateValue(e.target.value)}
|
||||
onBlur={() => {
|
||||
|
||||
@@ -46,7 +46,7 @@ const EphemeralExplorer = memo((props: { args: PathParams }) => {
|
||||
'search.ephemeralPaths',
|
||||
{
|
||||
path: path ?? (os === 'windows' ? 'C:\\' : '/'),
|
||||
withHiddenFiles: true,
|
||||
withHiddenFiles: false,
|
||||
order: settingsSnapshot.order
|
||||
}
|
||||
],
|
||||
@@ -94,7 +94,7 @@ const EphemeralExplorer = memo((props: { args: PathParams }) => {
|
||||
label="Add path as an indexed location"
|
||||
className="w-max min-w-0 shrink"
|
||||
>
|
||||
<AddLocationButton path={path} />
|
||||
<AddLocationButton className="ml-2" path={path} />
|
||||
</Tooltip>
|
||||
}
|
||||
right={<DefaultTopBarOptions />}
|
||||
|
||||
@@ -14,7 +14,6 @@ const pageRoutes: RouteObject = {
|
||||
{ path: 'media', lazy: () => import('./media') },
|
||||
{ path: 'spaces', lazy: () => import('./spaces') },
|
||||
{ path: 'debug', lazy: () => import('./debug') },
|
||||
{ path: 'spacedrop', lazy: () => import('./spacedrop') },
|
||||
{ path: 'sync', lazy: () => import('./sync') }
|
||||
]
|
||||
};
|
||||
@@ -26,6 +25,7 @@ const explorerRoutes: RouteObject[] = [
|
||||
{ path: 'node/:id', lazy: () => import('./node/$id') },
|
||||
{ path: 'tag/:id', lazy: () => import('./tag/$id') },
|
||||
{ path: 'ephemeral/:id', lazy: () => import('./ephemeral') },
|
||||
{ path: 'network/:id', lazy: () => import('./network') },
|
||||
{ path: 'search', lazy: () => import('./search') }
|
||||
];
|
||||
|
||||
|
||||
84
interface/app/$libraryId/network.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Globe } from '@sd/assets/icons';
|
||||
import { memo, Suspense, useDeferredValue, useMemo } from 'react';
|
||||
import { useDiscoveredPeers } from '@sd/client';
|
||||
import { PathParamsSchema, type PathParams } from '~/app/route-schemas';
|
||||
import { useOperatingSystem, useZodSearchParams } from '~/hooks';
|
||||
|
||||
import Explorer from './Explorer';
|
||||
import { ExplorerContextProvider } from './Explorer/Context';
|
||||
import { createDefaultExplorerSettings, nonIndexedPathOrderingSchema } from './Explorer/store';
|
||||
import { DefaultTopBarOptions } from './Explorer/TopBarOptions';
|
||||
import { useExplorer, useExplorerSettings } from './Explorer/useExplorer';
|
||||
import { TopBarPortal } from './TopBar/Portal';
|
||||
|
||||
const Network = memo((props: { args: PathParams }) => {
|
||||
const os = useOperatingSystem();
|
||||
|
||||
const discoveredPeers = useDiscoveredPeers();
|
||||
const peers = useMemo(() => Array.from(discoveredPeers.values()), [discoveredPeers]);
|
||||
|
||||
const explorerSettings = useExplorerSettings({
|
||||
settings: useMemo(
|
||||
() =>
|
||||
createDefaultExplorerSettings({
|
||||
order: {
|
||||
field: 'name',
|
||||
value: 'Asc'
|
||||
}
|
||||
}),
|
||||
[]
|
||||
),
|
||||
orderingKeys: nonIndexedPathOrderingSchema
|
||||
});
|
||||
|
||||
const explorer = useExplorer({
|
||||
items: peers.map((peer) => ({
|
||||
type: 'SpacedropPeer',
|
||||
has_local_thumbnail: false,
|
||||
thumbnail_key: null,
|
||||
item: {
|
||||
...peer,
|
||||
pub_id: []
|
||||
}
|
||||
})),
|
||||
settings: explorerSettings
|
||||
});
|
||||
|
||||
return (
|
||||
<ExplorerContextProvider explorer={explorer}>
|
||||
<TopBarPortal
|
||||
left={
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={Globe} className="mt-[-1px] h-[22px] w-[22px]" />
|
||||
<span className="truncate text-sm font-medium">Network</span>
|
||||
</div>
|
||||
}
|
||||
right={<DefaultTopBarOptions />}
|
||||
noSearch={true}
|
||||
/>
|
||||
<Explorer
|
||||
emptyNotice={
|
||||
<div className="flex h-full flex-col items-center justify-center text-white">
|
||||
<img src={Globe} className="h-32 w-32" />
|
||||
<h1 className="mt-4 text-lg font-bold">Your Local Network</h1>
|
||||
<p className="mt-1 max-w-sm text-center text-sm text-ink-dull">
|
||||
Other Spacedrive nodes on your LAN will appear here, along with your
|
||||
default OS network mounts.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</ExplorerContextProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export const Component = () => {
|
||||
const [pathParams] = useZodSearchParams(PathParamsSchema);
|
||||
const path = useDeferredValue(pathParams);
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<Network args={path} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Laptop } from '@sd/assets/icons';
|
||||
import { useMemo } from 'react';
|
||||
import { useBridgeQuery, useLibraryQuery } from '@sd/client';
|
||||
import { ExplorerItem, useBridgeQuery, useLibraryQuery } from '@sd/client';
|
||||
import { NodeIdParamsSchema } from '~/app/route-schemas';
|
||||
import { useZodRouteParams } from '~/hooks';
|
||||
|
||||
|
||||
@@ -19,12 +19,13 @@ export const CategoryList = [
|
||||
'Favorites',
|
||||
'Albums',
|
||||
'Photos',
|
||||
'Screenshots',
|
||||
'Videos',
|
||||
'Movies',
|
||||
'Music',
|
||||
'Documents',
|
||||
'Downloads',
|
||||
'Encrypted',
|
||||
'Documents',
|
||||
'Projects',
|
||||
'Applications',
|
||||
// 'Archives',
|
||||
|
||||
@@ -37,7 +37,8 @@ export const IconForCategory: Partial<Record<Category, string>> = {
|
||||
Encrypted: iconNames.Lock,
|
||||
Databases: iconNames.Database,
|
||||
Projects: iconNames.Folder,
|
||||
Trash: iconNames.Trash
|
||||
Trash: iconNames.Trash,
|
||||
Screenshots: iconNames.Screenshot
|
||||
};
|
||||
|
||||
export const IconToDescription = {
|
||||
@@ -58,7 +59,8 @@ export const IconToDescription = {
|
||||
Games: 'View all games in your library',
|
||||
Books: 'View all books in your library',
|
||||
Contacts: 'View all contacts in your library',
|
||||
Trash: 'View all files in your trash'
|
||||
Trash: 'View all files in your trash',
|
||||
Screenshots: 'View all screenshots in your library'
|
||||
};
|
||||
|
||||
export const OBJECT_CATEGORIES: Category[] = ['Recents', 'Favorites'];
|
||||
|
||||
@@ -41,7 +41,7 @@ export const Component = () => {
|
||||
<TopBarPortal right={<DefaultTopBarOptions />} />
|
||||
|
||||
<Statistics />
|
||||
|
||||
{/* <div className="mt-2 w-full" /> */}
|
||||
<Categories selected={selectedCategory} onSelectedChanged={setSelectedCategory} />
|
||||
|
||||
<div className="flex flex-1">
|
||||
|
||||
@@ -138,19 +138,18 @@ export const Component = () => {
|
||||
</div> */}
|
||||
</div>
|
||||
</Card>
|
||||
{(isDev || debugState.enabled) && (
|
||||
<Setting
|
||||
mini
|
||||
title="Debug mode"
|
||||
description="Enable extra debugging features within the app."
|
||||
>
|
||||
<Switch
|
||||
size="md"
|
||||
checked={debugState.enabled}
|
||||
onClick={() => (getDebugState().enabled = !debugState.enabled)}
|
||||
/>
|
||||
</Setting>
|
||||
)}
|
||||
|
||||
<Setting
|
||||
mini
|
||||
title="Debug mode"
|
||||
description="Enable extra debugging features within the app."
|
||||
>
|
||||
<Switch
|
||||
size="md"
|
||||
checked={debugState.enabled}
|
||||
onClick={() => (getDebugState().enabled = !debugState.enabled)}
|
||||
/>
|
||||
</Setting>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,9 +13,7 @@ export const Component = () => {
|
||||
<Setting
|
||||
mini
|
||||
title="Share Additional Telemetry and Usage Data"
|
||||
description="Enable to share extra usage information and telemetry with developers in order to further improve the app.
|
||||
If disabled, the only data sent is that you are an active user, which version of the app and core you're using, and which platform you're on
|
||||
(e.g. mobile, web or desktop)."
|
||||
description="Toggle ON to provide developers with detailed usage and telemetry data to enhance the app. Toggle OFF to send only basic data: your activity status, app version, core version, and platform (e.g., mobile, web, or desktop)."
|
||||
>
|
||||
<Switch
|
||||
checked={fullTelemetry}
|
||||
|
||||
@@ -83,10 +83,10 @@ export const Component = () => {
|
||||
<Divider />
|
||||
<div>
|
||||
<h1 className="my-5 text-lg font-bold text-ink">
|
||||
We also would like to thank all our contributors
|
||||
Meet the contributors behind Spacedrive
|
||||
</h1>
|
||||
<img
|
||||
src="https://contrib.rocks/image?repo=spacedriveapp/spacedrive&columns=12"
|
||||
src="https://contrib.rocks/image?repo=spacedriveapp/spacedrive&columns=12&anon=1"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
.honeycomb-outer {
|
||||
font-size: 0; /*disable white space between inline block element */
|
||||
display: flex;
|
||||
--s: 150px; /* size */
|
||||
--m: 4px; /* margin */
|
||||
--f: calc(1.732 * var(--s) + 4 * var(--m) - 1px);
|
||||
}
|
||||
|
||||
.honeycomb-container .honeycomb-item {
|
||||
width: var(--s);
|
||||
margin: var(--m);
|
||||
height: calc(var(--s) * 1.1547);
|
||||
display: inline-block;
|
||||
clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
|
||||
// background: rgba(48, 48, 55, 0.272);
|
||||
margin-bottom: calc(var(--m) - var(--s) * 0.2885);
|
||||
}
|
||||
|
||||
.honeycomb-container::before {
|
||||
content: '';
|
||||
width: calc(var(--s) / 2 + var(--m));
|
||||
float: left;
|
||||
height: 120%;
|
||||
shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px), #000 0 var(--f));
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { Icon, User } from '@phosphor-icons/react';
|
||||
import { GoogleDrive, iCloud, Mega } from '@sd/assets/images';
|
||||
import clsx from 'clsx';
|
||||
import { tw } from '@sd/ui';
|
||||
import { SubtleButton, SubtleButtonContainer } from '~/components';
|
||||
import { OperatingSystem } from '~/util/Platform';
|
||||
|
||||
import classes from './spacedrop.module.scss';
|
||||
|
||||
// const { Form, Input, useZodForm, z } = forms;
|
||||
|
||||
// TODO: move this to UI, copied from Inspector
|
||||
const Pill = tw.span`mt-1 inline border border-transparent px-0.5 text-[9px] font-medium shadow shadow-app-shade/5 bg-app-selected rounded text-ink-dull`;
|
||||
|
||||
type DropItemProps = {
|
||||
// TODO: remove optionals when dummy data is removed (except for icon)
|
||||
name?: string;
|
||||
connectionType?: 'lan' | 'bluetooth' | 'usb' | 'p2p' | 'cloud';
|
||||
receivingNodeOsType?: Omit<OperatingSystem, 'unknown'>;
|
||||
} & ({ image: string } | { icon?: Icon } | { brandIcon: string });
|
||||
|
||||
function DropItem(props: DropItemProps) {
|
||||
let icon;
|
||||
if ('image' in props) {
|
||||
icon = <img className="rounded-full" src={props.image} alt={props.name} />;
|
||||
} else if ('brandIcon' in props) {
|
||||
let brandIconSrc;
|
||||
switch (props.brandIcon) {
|
||||
case 'google-drive':
|
||||
brandIconSrc = GoogleDrive;
|
||||
break;
|
||||
case 'icloud':
|
||||
brandIconSrc = iCloud;
|
||||
break;
|
||||
case 'mega':
|
||||
brandIconSrc = Mega;
|
||||
break;
|
||||
}
|
||||
if (brandIconSrc) {
|
||||
icon = (
|
||||
<div className="flex h-full items-center justify-center p-3">
|
||||
<img className="rounded-full " src={brandIconSrc} alt={props.name} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
//
|
||||
const Icon = props.icon || User;
|
||||
icon = <Icon className={clsx('m-3 h-8 w-8', !props.name && 'opacity-20')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.honeycombItem,
|
||||
'overflow-hidden bg-app-box/20 hover:bg-app-box/50'
|
||||
)}
|
||||
>
|
||||
<div className="group relative flex h-full w-full flex-col items-center justify-center ">
|
||||
{/* <SubtleButtonContainer className="absolute left-[12px] top-[55px]">
|
||||
<SubtleButton icon={Star} />
|
||||
</SubtleButtonContainer> */}
|
||||
<div className="h-14 w-14 rounded-full bg-app-button">{icon}</div>
|
||||
<SubtleButtonContainer className="absolute right-[12px] top-[55px] rotate-90">
|
||||
<SubtleButton />
|
||||
</SubtleButtonContainer>
|
||||
{props.name && <span className="mt-1 text-xs font-medium">{props.name}</span>}
|
||||
<div className="flex flex-row space-x-1">
|
||||
{props.receivingNodeOsType && <Pill>{props.receivingNodeOsType}</Pill>}
|
||||
{props.connectionType && (
|
||||
<Pill
|
||||
className={clsx(
|
||||
'uppercase !text-white',
|
||||
props.connectionType === 'lan' && 'bg-green-500',
|
||||
props.connectionType === 'p2p' && 'bg-blue-500'
|
||||
)}
|
||||
>
|
||||
{props.connectionType}
|
||||
</Pill>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Component = () => {
|
||||
return (
|
||||
<>
|
||||
<div className={classes.honeycombOuter}>
|
||||
<div className={clsx(classes.honeycombContainer, 'mt-8')}></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
BIN
packages/assets/icons/AmazonS3.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
packages/assets/icons/BackBlaze.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
packages/assets/icons/Box.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
packages/assets/icons/DAV.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
packages/assets/icons/Dropbox.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
packages/assets/icons/Globe.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
packages/assets/icons/GlobeAlt.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
packages/assets/icons/GoogleDrive.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
packages/assets/icons/HDD.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
packages/assets/icons/Home.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
packages/assets/icons/Mega.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
packages/assets/icons/OneDrive.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
packages/assets/icons/OpenStack.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
packages/assets/icons/PCloud.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
packages/assets/icons/SD.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
packages/assets/icons/Screenshot.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
packages/assets/icons/ScreenshotAlt.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
packages/assets/icons/Spacedrop.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
@@ -2,24 +2,29 @@
|
||||
* This file was automatically generated by a script.
|
||||
* To regenerate this file, run: pnpm assets gen
|
||||
*/
|
||||
|
||||
import Album_Light from './Album_Light.png';
|
||||
import Album from './Album.png';
|
||||
import Alias_Light from './Alias_Light.png';
|
||||
import Alias from './Alias.png';
|
||||
import AmazonS3 from './AmazonS3.png';
|
||||
import Application_Light from './Application_Light.png';
|
||||
import Application from './Application.png';
|
||||
import Archive_Light from './Archive_Light.png';
|
||||
import Archive from './Archive.png';
|
||||
import Audio_Light from './Audio_Light.png';
|
||||
import Audio from './Audio.png';
|
||||
import BackBlaze from './BackBlaze.png';
|
||||
import Ball from './Ball.png';
|
||||
import Book_Light from './Book_Light.png';
|
||||
import Book from './Book.png';
|
||||
import BookBlue from './BookBlue.png';
|
||||
import Box from './Box.png';
|
||||
import Collection_Light from './Collection_Light.png';
|
||||
import Collection from './Collection.png';
|
||||
import Database_Light from './Database_Light.png';
|
||||
import Database from './Database.png';
|
||||
import DAV from './DAV.png';
|
||||
import Document_doc_Light from './Document_doc_Light.png';
|
||||
import Document_doc from './Document_doc.png';
|
||||
import Document_Light from './Document_Light.png';
|
||||
@@ -30,6 +35,7 @@ import Document_xls from './Document_xls.png';
|
||||
import Document from './Document.png';
|
||||
import Drive_Light from './Drive_Light.png';
|
||||
import Drive from './Drive.png';
|
||||
import Dropbox from './Dropbox.png';
|
||||
import Encrypted_Light from './Encrypted_Light.png';
|
||||
import Encrypted from './Encrypted.png';
|
||||
import Entity_Light from './Entity_Light.png';
|
||||
@@ -45,8 +51,13 @@ import FolderGrey_Light from './FolderGrey_Light.png';
|
||||
import FolderGrey from './FolderGrey.png';
|
||||
import Game_Light from './Game_Light.png';
|
||||
import Game from './Game.png';
|
||||
import Globe from './Globe.png';
|
||||
import GlobeAlt from './GlobeAlt.png';
|
||||
import GoogleDrive from './GoogleDrive.png';
|
||||
import HDD from './HDD.png';
|
||||
import Heart_Light from './Heart_Light.png';
|
||||
import Heart from './Heart.png';
|
||||
import Home from './Home.png';
|
||||
import Image_Light from './Image_Light.png';
|
||||
import Image from './Image.png';
|
||||
import Key_Light from './Key_Light.png';
|
||||
@@ -59,6 +70,7 @@ import Link_Light from './Link_Light.png';
|
||||
import Link from './Link.png';
|
||||
import Lock_Light from './Lock_Light.png';
|
||||
import Lock from './Lock.png';
|
||||
import Mega from './Mega.png';
|
||||
import Mesh_Light from './Mesh_Light.png';
|
||||
import Mesh from './Mesh.png';
|
||||
import Mobile_Light from './Mobile_Light.png';
|
||||
@@ -67,12 +79,19 @@ import Movie_Light from './Movie_Light.png';
|
||||
import Movie from './Movie.png';
|
||||
import Node_Light from './Node_Light.png';
|
||||
import Node from './Node.png';
|
||||
import OneDrive from './OneDrive.png';
|
||||
import OpenStack from './OpenStack.png';
|
||||
import Package_Light from './Package_Light.png';
|
||||
import Package from './Package.png';
|
||||
import PCloud from './PCloud.png';
|
||||
import Scrapbook_Light from './Scrapbook_Light.png';
|
||||
import Scrapbook from './Scrapbook.png';
|
||||
import Screenshot from './Screenshot.png';
|
||||
import ScreenshotAlt from './ScreenshotAlt.png';
|
||||
import SD from './SD.png';
|
||||
import Server_Light from './Server_Light.png';
|
||||
import Server from './Server.png';
|
||||
import Spacedrop from './Spacedrop.png';
|
||||
import Tablet_Light from './Tablet_Light.png';
|
||||
import Tablet from './Tablet.png';
|
||||
import Tags_Light from './Tags_Light.png';
|
||||
@@ -100,18 +119,22 @@ export {
|
||||
Album_Light,
|
||||
Alias,
|
||||
Alias_Light,
|
||||
AmazonS3,
|
||||
Application,
|
||||
Application_Light,
|
||||
Archive,
|
||||
Archive_Light,
|
||||
Audio,
|
||||
Audio_Light,
|
||||
BackBlaze,
|
||||
Ball,
|
||||
Book,
|
||||
BookBlue,
|
||||
Book_Light,
|
||||
Box,
|
||||
Collection,
|
||||
Collection_Light,
|
||||
DAV,
|
||||
Database,
|
||||
Database_Light,
|
||||
Document,
|
||||
@@ -124,6 +147,7 @@ export {
|
||||
Document_xls_Light,
|
||||
Drive,
|
||||
Drive_Light,
|
||||
Dropbox,
|
||||
Encrypted,
|
||||
Encrypted_Light,
|
||||
Entity,
|
||||
@@ -139,8 +163,13 @@ export {
|
||||
Folder_Light,
|
||||
Game,
|
||||
Game_Light,
|
||||
Globe,
|
||||
GlobeAlt,
|
||||
GoogleDrive,
|
||||
HDD,
|
||||
Heart,
|
||||
Heart_Light,
|
||||
Home,
|
||||
Image,
|
||||
Image_Light,
|
||||
Key,
|
||||
@@ -153,6 +182,7 @@ export {
|
||||
Link_Light,
|
||||
Lock,
|
||||
Lock_Light,
|
||||
Mega,
|
||||
Mesh,
|
||||
Mesh_Light,
|
||||
Mobile,
|
||||
@@ -161,12 +191,19 @@ export {
|
||||
Movie_Light,
|
||||
Node,
|
||||
Node_Light,
|
||||
OneDrive,
|
||||
OpenStack,
|
||||
PCloud,
|
||||
Package,
|
||||
Package_Light,
|
||||
SD,
|
||||
Scrapbook,
|
||||
Scrapbook_Light,
|
||||
Screenshot,
|
||||
ScreenshotAlt,
|
||||
Server,
|
||||
Server_Light,
|
||||
Spacedrop,
|
||||
Tablet,
|
||||
Tablet_Light,
|
||||
Tags,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* This file was automatically generated by a script.
|
||||
* To regenerate this file, run: pnpm assets gen
|
||||
*/
|
||||
|
||||
import AlphaBg_Light from './AlphaBg_Light.png';
|
||||
import AlphaBg from './AlphaBg.png';
|
||||
import AppLogo from './AppLogo.png';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* This file was automatically generated by a script.
|
||||
* To regenerate this file, run: pnpm assets gen
|
||||
*/
|
||||
|
||||
import { ReactComponent as Academia } from './Academia.svg';
|
||||
import { ReactComponent as Discord } from './Discord.svg';
|
||||
import { ReactComponent as Dribbble } from './Dribbble.svg';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* This file was automatically generated by a script.
|
||||
* To regenerate this file, run: pnpm assets gen
|
||||
*/
|
||||
|
||||
import { ReactComponent as angular } from './angular.svg';
|
||||
import { ReactComponent as bun } from './bun.svg';
|
||||
import { ReactComponent as c } from './c.svg';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* This file was automatically generated by a script.
|
||||
* To regenerate this file, run: pnpm assets gen
|
||||
*/
|
||||
|
||||
import { ReactComponent as ace } from './ace.svg';
|
||||
import { ReactComponent as acemanifest } from './acemanifest.svg';
|
||||
import { ReactComponent as adoc } from './adoc.svg';
|
||||
|
||||
@@ -12,6 +12,12 @@ export const iconNames = Object.fromEntries(
|
||||
.map((key) => [key, key]) // Map key to [key, key] format
|
||||
) as Record<IconTypes, string>;
|
||||
|
||||
export const getIconByName = (name: IconTypes, isDark?: boolean) => {
|
||||
let _name = name;
|
||||
if (!isDark) _name = (name + '_Light') as IconTypes;
|
||||
return icons[name];
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the appropriate icon based on the given criteria.
|
||||
*
|
||||
|
||||
@@ -122,7 +122,7 @@ export type CRDTOperationType = SharedOperation | RelationOperation
|
||||
/**
|
||||
* Meow
|
||||
*/
|
||||
export type Category = "Recents" | "Favorites" | "Albums" | "Photos" | "Videos" | "Movies" | "Music" | "Documents" | "Downloads" | "Encrypted" | "Projects" | "Applications" | "Archives" | "Databases" | "Games" | "Books" | "Contacts" | "Trash"
|
||||
export type Category = "Recents" | "Favorites" | "Albums" | "Photos" | "Videos" | "Movies" | "Music" | "Documents" | "Downloads" | "Encrypted" | "Projects" | "Applications" | "Archives" | "Databases" | "Games" | "Books" | "Contacts" | "Trash" | "Screenshots"
|
||||
|
||||
export type ChangeNodeNameArgs = { name: string | null }
|
||||
|
||||
@@ -153,7 +153,7 @@ export type Error = { code: ErrorCode; message: string }
|
||||
*/
|
||||
export type ErrorCode = "BadRequest" | "Unauthorized" | "Forbidden" | "NotFound" | "Timeout" | "Conflict" | "PreconditionFailed" | "PayloadTooLarge" | "MethodNotSupported" | "ClientClosedRequest" | "InternalServerError"
|
||||
|
||||
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 } | { type: "NonIndexedPath"; has_local_thumbnail: boolean; thumbnail_key: string[] | null; item: NonIndexedPathItem }
|
||||
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 } | { type: "NonIndexedPath"; has_local_thumbnail: boolean; thumbnail_key: string[] | null; item: NonIndexedPathItem } | { type: "SpacedropPeer"; has_local_thumbnail: boolean; thumbnail_key: string[] | null; item: PeerMetadata }
|
||||
|
||||
export type ExplorerLayout = "grid" | "list" | "media"
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ export function getIndexedItemFilePath(data: ExplorerItem) {
|
||||
export function getItemLocation(data: ExplorerItem) {
|
||||
return data.type === 'Location' ? data.item : null;
|
||||
}
|
||||
export function getItemSpacedropPeer(data: ExplorerItem) {
|
||||
return data.type === 'SpacedropPeer' ? data.item : null;
|
||||
}
|
||||
|
||||
export function getExplorerItemData(data?: null | ExplorerItem) {
|
||||
const itemObj = data ? getItemObject(data) : null;
|
||||
@@ -40,11 +43,15 @@ export function getExplorerItemData(data?: null | ExplorerItem) {
|
||||
extension: null as string | null,
|
||||
locationId: null as number | null,
|
||||
dateIndexed: null as string | null,
|
||||
dateCreated: data?.item.date_created ?? itemObj?.date_created ?? null,
|
||||
dateCreated:
|
||||
data?.item && 'date_created' in data.item
|
||||
? data.item.date_created
|
||||
: itemObj?.date_created ?? null,
|
||||
dateModified: null as string | null,
|
||||
dateAccessed: itemObj?.date_accessed ?? null,
|
||||
thumbnailKey: data?.thumbnail_key ?? [],
|
||||
hasLocalThumbnail: data?.has_local_thumbnail ?? false // this will be overwritten if new thumbnail is generated
|
||||
hasLocalThumbnail: data?.has_local_thumbnail ?? false, // this will be overwritten if new thumbnail is generated
|
||||
customIcon: null as string | null
|
||||
};
|
||||
|
||||
if (!data) return itemData;
|
||||
@@ -72,6 +79,9 @@ export function getExplorerItemData(data?: null | ExplorerItem) {
|
||||
itemData.isDir = true;
|
||||
itemData.locationId = location.id;
|
||||
itemData.dateIndexed = location.date_created;
|
||||
} else if (data.type === 'SpacedropPeer') {
|
||||
itemData.name = data.item.name;
|
||||
itemData.customIcon = 'Laptop';
|
||||
}
|
||||
|
||||
if (data.type == 'Path' && itemData.isDir) itemData.kind = 'Folder';
|
||||
|
||||
@@ -24,7 +24,9 @@ export enum ObjectKindEnum {
|
||||
Code,
|
||||
Database,
|
||||
Book,
|
||||
Config
|
||||
Config,
|
||||
Dotfile,
|
||||
Screenshot
|
||||
}
|
||||
|
||||
export type ObjectKindKey = keyof typeof ObjectKindEnum;
|
||||
|
||||