[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>
This commit is contained in:
Jamie Pine
2023-10-09 03:11:23 -07:00
committed by GitHub
parent bd7e2c1796
commit 10a10c56ad
70 changed files with 618 additions and 433 deletions

BIN
Cargo.lock generated
View File

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

@@ -141,6 +141,7 @@ impl Node {
node.p2p.start(p2p_stream, node.clone());
let router = api::mount();
info!("Spacedrive online.");
Ok((node, router))
}

View File

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

View File

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

View File

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

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

View File

@@ -52,4 +52,8 @@ pub enum ObjectKind {
Book = 22,
/// Config file
Config = 23,
/// Dotfile
Dotfile = 24,
/// Screenshot
Screenshot = 25,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -19,12 +19,13 @@ export const CategoryList = [
'Favorites',
'Albums',
'Photos',
'Screenshots',
'Videos',
'Movies',
'Music',
'Documents',
'Downloads',
'Encrypted',
'Documents',
'Projects',
'Applications',
// 'Archives',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,9 @@ export enum ObjectKindEnum {
Code,
Database,
Book,
Config
Config,
Dotfile,
Screenshot
}
export type ObjectKindKey = keyof typeof ObjectKindEnum;

1
packages/test-files Submodule

Submodule packages/test-files added at 146fbb543f

BIN
pnpm-lock.yaml generated
View File

Binary file not shown.