lazy load icons, refactor store,

This commit is contained in:
Jamie
2021-10-23 14:30:36 -07:00
parent 1a62387711
commit 1d418fe60f
27 changed files with 617 additions and 299 deletions

2
.TODO
View File

@@ -18,6 +18,8 @@
☐ Job queue system
☐ Native file previews
☐ Secret keystore
☐ Search
Efficient way to search sqlite: make file table WITHOUT ROWID
☐ File encryptor
☐ File viewer / player
☐ Open with

View File

@@ -40,6 +40,7 @@
"react-dom": "^17.0.2",
"react-dropzone": "^11.3.4",
"react-router-dom": "^5.2.0",
"react-spline": "^1.2.1",
"react-virtualized": "^9.22.3",
"rooks": "^5.7.1",
"tailwindcss": "^2.2.16",

BIN
src-tauri/Cargo.lock generated
View File

Binary file not shown.

View File

@@ -1,11 +1,11 @@
[package]
name = "SpaceDrive"
name = "spacedrive"
version = "0.1.0"
description = "The next gen private virtual filesystem."
authors = ["you"]
license = ""
repository = ""
default-run = "SpaceDrive"
default-run = "spacedrive"
edition = "2018"
build = "src/build.rs"

View File

@@ -1,30 +1,39 @@
use crate::db::entity::file;
use crate::db::connection::db_instance;
use crate::filesystem;
use crate::filesystem::retrieve::Directory;
use crate::swift::get_file_thumbnail_base64;
use crate::{db, filesystem};
use anyhow::Result;
use once_cell::sync::OnceCell;
use sea_orm::ColumnTrait;
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter};
pub static DB_INSTANCE: OnceCell<DatabaseConnection> = OnceCell::new();
use serde::Serialize;
async fn db_instance() -> Result<&'static DatabaseConnection, String> {
if DB_INSTANCE.get().is_none() {
let db = db::connection::get_connection()
.await
.map_err(|e| e.to_string())?;
DB_INSTANCE.set(db).unwrap_or_default();
Ok(DB_INSTANCE.get().unwrap())
} else {
Ok(DB_INSTANCE.get().unwrap())
}
#[derive(Serialize)]
pub enum GlobalEventKind {
FileTypeThumb,
}
#[derive(Serialize)]
pub struct GlobalEvent<T> {
pub kind: GlobalEventKind,
pub data: T,
}
#[derive(Serialize)]
pub struct GenFileTypeIconsResponse {
pub thumbnail_b64: String,
pub file_id: u32,
}
pub fn reply<T: Serialize>(window: &tauri::Window, kind: GlobalEventKind, data: T) {
let _message = window
.emit("message", GlobalEvent { kind, data })
.map_err(|e| println!("{}", e));
}
#[tauri::command(async)]
pub async fn scan_dir(path: String) -> Result<(), String> {
pub async fn scan_dir(window: tauri::Window, path: String) -> Result<(), String> {
db_instance().await?;
// reply(&window, GlobalEventKind::JEFF, "jeff");
let files = filesystem::indexer::scan(&path)
.await
.map_err(|e| e.to_string())?;
@@ -35,37 +44,31 @@ pub async fn scan_dir(path: String) -> Result<(), String> {
}
#[tauri::command(async)]
pub async fn get_file_thumb(path: &str) -> Result<String, String> {
let thumbnail_b46 = get_file_thumbnail_base64(path).to_string();
let thumbnail_b64 = get_file_thumbnail_base64(path).to_string();
Ok(thumbnail_b46)
Ok(thumbnail_b64)
}
#[tauri::command(async)]
pub async fn get_files(path: String) -> Result<Directory, String> {
let connection = db_instance().await?;
Ok(filesystem::retrieve::get_dir_with_contents(&path).await?)
}
println!("getting files... {:?}", &path);
let directories = file::Entity::find()
.filter(file::Column::Uri.eq(path))
.all(connection)
.await
.map_err(|e| e.to_string())?;
if directories.is_empty() {
return Err("fuk".to_owned());
#[tauri::command]
pub async fn get_thumbs_for_directory(window: tauri::Window, path: &str) -> Result<(), String> {
let dir = filesystem::retrieve::get_dir_with_contents(&path).await?;
for file in dir.contents.into_iter() {
let thumbnail_b64 = get_file_thumbnail_base64(&file.uri).to_string();
println!("getting thumb: {:?}", file.id);
reply(
&window,
GlobalEventKind::FileTypeThumb,
GenFileTypeIconsResponse {
thumbnail_b64,
file_id: file.id,
},
)
}
let directory = &directories[0];
let files = file::Entity::find()
.filter(file::Column::ParentId.eq(directory.id))
.all(connection)
.await
.map_err(|e| e.to_string())?;
Ok(Directory {
directory: directory.clone(),
contents: files,
})
Ok(())
}

View File

@@ -1,5 +1,6 @@
use crate::app::config;
use anyhow::{Context, Result};
use once_cell::sync::OnceCell;
use rusqlite::Connection;
use sea_orm::{Database, DatabaseConnection, DbErr};
// use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode};
@@ -16,6 +17,17 @@ pub async fn get_connection() -> Result<DatabaseConnection, DbErr> {
Ok(db)
}
pub static DB_INSTANCE: OnceCell<DatabaseConnection> = OnceCell::new();
pub async fn db_instance() -> Result<&'static DatabaseConnection, String> {
if DB_INSTANCE.get().is_none() {
let db = get_connection().await.map_err(|e| e.to_string())?;
DB_INSTANCE.set(db).unwrap_or_default();
Ok(DB_INSTANCE.get().unwrap())
} else {
Ok(DB_INSTANCE.get().unwrap())
}
}
pub async fn create_primary_db() -> Result<(), sqlx::Error> {
let config = config::get_config();

View File

@@ -1,4 +1,4 @@
use crate::commands::DB_INSTANCE;
use crate::db::connection::DB_INSTANCE;
use crate::db::entity::file;
use crate::filesystem::{checksum, init};
use crate::util::time;
@@ -24,10 +24,18 @@ fn is_app_bundle(entry: &DirEntry) -> bool {
let contains_dot = entry
.file_name()
.to_str()
.map(|s| s.contains("."))
.map(|s| s.contains(".app") | s.contains(".bundle"))
.unwrap_or(false);
is_dir && contains_dot
let is_app_bundle = is_dir && contains_dot;
// if is_app_bundle {
// let path_buff = entry.path();
// let path = path_buff.to_str().unwrap();
// self::path(&path, );
// }
is_app_bundle
}
pub async fn scan(path: &str) -> Result<()> {
@@ -42,33 +50,50 @@ pub async fn scan(path: &str) -> Result<()> {
// insert root directory
dirs.insert(path.to_owned(), file.id);
// iterate over files and subdirectories
for entry in WalkDir::new(path)
.into_iter()
.filter_entry(|e| !is_hidden(e) && !is_app_bundle(e))
{
for entry in WalkDir::new(path).into_iter().filter_entry(|dir| {
let approved = !is_hidden(dir) && !is_app_bundle(dir);
approved
}) {
let entry = entry?;
let path_buff = entry.path();
let path = path_buff.to_str().unwrap();
// get the parent directory from the path
let parent = path_buff.parent().unwrap().to_str().unwrap();
// get parent dir database id from hashmap
let parent_dir = dirs.get(&parent.to_owned());
let child_path = entry.path().to_str().unwrap();
let parent_dir = get_parent_dir_id(&dirs, &entry);
// analyse the child file
let child_file = self::path(&path, parent_dir).await.unwrap();
let child_file = self::path(&child_path, parent_dir).await.unwrap();
println!(
"Reading file from dir {:?} {:?} assigned id {:?}",
parent_dir, child_path, child_file.id
);
// if this file is a directory, save in hashmap with database id
if child_file.is_dir {
dirs.insert(path.to_owned(), child_file.id);
dirs.insert(child_path.to_owned(), child_file.id);
}
// println!("{}", entry.path().display());
}
}
println!("Scanning complete: {}", &path);
Ok(())
}
fn get_parent_dir_id(dirs: &HashMap<String, u32>, entry: &DirEntry) -> Option<u32> {
let path = entry.path();
let parent_path = path
.parent()
.unwrap_or_else(|| path)
.to_str()
.unwrap_or_default();
let parent_dir_id = dirs.get(&parent_path.to_owned());
match parent_dir_id {
Some(x) => Some(x.clone()),
None => None,
}
}
// Read a file from path returning the File struct
// Generates meta checksum and extracts metadata
pub async fn path(path: &str, parent_id: Option<&u32>) -> Result<file::Model> {
pub async fn path(path: &str, parent_id: Option<u32>) -> Result<file::Model> {
let db = DB_INSTANCE.get().unwrap();
let path_buff = path::PathBuf::from(path);
@@ -99,9 +124,7 @@ pub async fn path(path: &str, parent_id: Option<&u32>) -> Result<file::Model> {
..Default::default()
};
let file = file.save(&db).await.unwrap();
println!("FILE: {:?}", file);
let _file = file.save(&db).await.unwrap();
// REPLACE WHEN SEA QL PULLS THROUGH
let existing_files = file::Entity::find()

View File

@@ -1,4 +1,8 @@
use crate::db::connection::db_instance;
use crate::db::entity::file;
use anyhow::Result;
use sea_orm::ColumnTrait;
use sea_orm::{EntityTrait, QueryFilter};
use serde::Serialize;
#[derive(Serialize)]
@@ -6,3 +10,32 @@ pub struct Directory {
pub directory: file::Model,
pub contents: Vec<file::Model>,
}
pub async fn get_dir_with_contents(path: &str) -> Result<Directory, String> {
let connection = db_instance().await?;
println!("getting files... {:?}", &path);
let directories = file::Entity::find()
.filter(file::Column::Uri.eq(path))
.all(connection)
.await
.map_err(|e| e.to_string())?;
if directories.is_empty() {
return Err("fuk".to_owned());
}
let directory = &directories[0];
let files = file::Entity::find()
.filter(file::Column::ParentId.eq(directory.id))
.all(connection)
.await
.map_err(|e| e.to_string())?;
Ok(Directory {
directory: directory.clone(),
contents: files,
})
}

View File

@@ -13,6 +13,8 @@ mod util;
use crate::app::menu;
use env_logger;
use futures::executor::block_on;
use tauri::Manager;
// use systemstat::{saturating_sub_bytes, Platform, System};
fn main() {
@@ -31,10 +33,17 @@ fn main() {
// block_on(filesystem::device::discover_storage_devices()).unwrap();
tauri::Builder::default()
.setup(|app| {
// let main_window = app.get_window("main").unwrap();
// // would need to emit this elsewhere in my Rust code
// main_window.emit("my-event", "payload");
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::scan_dir,
commands::get_files,
commands::get_file_thumb
commands::get_file_thumb,
commands::get_thumbs_for_directory
])
.menu(menu::get_menu())
.run(tauri::generate_context!())

View File

@@ -13,7 +13,7 @@
"bundle": {
"active": true,
"targets": "all",
"identifier": "co.sd.client",
"identifier": "co.spacedrive.client",
"icon": ["icons/icon.icns"],
"resources": [],
"externalBin": [],

View File

@@ -1,4 +1,4 @@
import React, { useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import { Route, BrowserRouter as Router, Switch, Redirect } from 'react-router-dom';
import { Sidebar } from './components/file/Sidebar';
import { TopBar } from './components/layout/TopBar';
@@ -6,11 +6,15 @@ import { useInputState } from './hooks/useInputState';
import { SettingsScreen } from './screens/Settings';
import { ExplorerScreen } from './screens/Explorer';
import { invoke } from '@tauri-apps/api';
import { DebugGlobalStore } from './store/Debug';
import { useGlobalEvents } from './hooks/useGlobalEvents';
export default function App() {
useGlobalEvents();
return (
<Router>
<div className="flex flex-col select-none h-screen rounded-xl border border-gray-200 dark:border-gray-450 bg-white text-gray-900 dark:text-white dark:bg-gray-800 overflow-hidden ">
<DebugGlobalStore />
<TopBar />
<div className="flex flex-row min-h-full">
<Sidebar />

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,13 @@ import { IFile } from '../../types';
import byteSize from 'pretty-bytes';
import { useKey } from 'rooks';
import { invoke } from '@tauri-apps/api';
import { useExplorerStore } from '../../store/explorer';
import {
useCurrentDir,
useExplorerStore,
useFile,
useSelectedFile,
useSelectedFileIndex
} from '../../store/explorer';
import { DirectoryResponse } from '../../screens/Explorer';
interface Column {
@@ -14,13 +20,6 @@ interface Column {
width: number;
}
function renderIcon(dirHash: string, path: string, rowIndex: number) {
const setIconForFile = useExplorerStore.getState().setIconForFile;
invoke('get_file_thumb', { path }).then((imageData) => {
if (dirHash) setIconForFile(dirHash, rowIndex, imageData as string);
});
}
// Function ensure no types are loss, but guarantees that they are Column[]
function ensureIsColumns<T extends Column[]>(data: T) {
return data;
@@ -40,38 +39,20 @@ export const FileList: React.FC<{}> = (props) => {
const scrollContainer = useRef<null | HTMLDivElement>(null);
const [rowHeight, setRowHeight] = useState(0);
// const [selectedRow, setSelectedRow] = useState(0);
const [currentDir, activeDirHash, collectDir, selectedRow, setSelectedRow] = useExplorerStore(
(state) => [
state.dirs[state.activeDirHash],
state.activeDirHash,
state.collectDir,
state.selected,
state.setSelected
]
);
const currentDir = useCurrentDir();
console.log({ currentDir });
if (!currentDir) return <></>;
const explorer = useExplorerStore.getState();
useKey('ArrowUp', (e) => {
e.preventDefault();
if (!selectedRow || !currentDir?.children) return;
if (selectedRow?.index > 0) {
const nextRowIndex = selectedRow.index - 1;
const row = currentDir.children[selectedRow.index - 1];
setSelectedRow(nextRowIndex, row);
if (!row.icon_b64) renderIcon(activeDirHash, row.uri, nextRowIndex);
}
// loop to bottom
else setSelectedRow(currentDir.children_count, currentDir.children[currentDir.children_count]);
if (explorer.selectedFile) explorer.selectFile(currentDir.id, explorer.selectedFile, 'above');
});
useKey('ArrowDown', (e) => {
e.preventDefault();
if (!selectedRow || !currentDir?.children) return;
// increment if rows below exist
if (selectedRow.index < currentDir.children_count) {
const nextRowIndex = selectedRow.index + 1;
const row = currentDir.children[selectedRow.index + 1];
setSelectedRow(nextRowIndex, row);
if (!row.icon_b64) renderIcon(activeDirHash, row.uri, nextRowIndex);
} else setSelectedRow(0, currentDir.children[0]);
if (explorer.selectedFile) explorer.selectFile(currentDir.id, explorer.selectedFile, 'below');
});
function isRowOutOfView(rowHeight: number, rowIndex: number) {
@@ -80,58 +61,48 @@ export const FileList: React.FC<{}> = (props) => {
function handleScroll() {}
return useMemo(
() => (
<div
ref={scrollContainer}
onScroll={handleScroll}
className="table-container w-full h-full overflow-scroll bg-white dark:bg-gray-900 p-3 cursor-default"
>
<div className="table-head">
<div className="table-head-row flex flex-row p-2">
{columns.map((col) => (
<div
key={col.key}
className="table-head-cell flex flex-row items-center relative group px-4"
style={{ width: col.width }}
>
<DotsVerticalIcon className="hidden absolute group-hover:block drag-handle w-5 h-5 opacity-10 -ml-5 cursor-move" />
<span className="text-sm text-gray-500 font-medium">{col.column}</span>
</div>
))}
</div>
</div>
<div className="table-body pb-10">
{currentDir?.children?.map((row, index) => (
<RenderRow key={row.id} row={row} rowIndex={index} dirId={currentDir.meta_checksum} />
return (
<div
ref={scrollContainer}
onScroll={handleScroll}
className="table-container w-full h-full overflow-scroll bg-white dark:bg-gray-900 p-3 cursor-default"
>
<div className="table-head">
<div className="table-head-row flex flex-row p-2">
{columns.map((col) => (
<div
key={col.key}
className="table-head-cell flex flex-row items-center relative group px-4"
style={{ width: col.width }}
>
<DotsVerticalIcon className="hidden absolute group-hover:block drag-handle w-5 h-5 opacity-10 -ml-5 cursor-move" />
<span className="text-sm text-gray-500 font-medium">{col.column}</span>
</div>
))}
</div>
</div>
),
[activeDirHash]
<div className="table-body pb-10">
{currentDir?.children?.map((row, index) => (
<RenderRow key={row.id} row={row} rowIndex={index} dirId={currentDir.id} />
))}
</div>
</div>
);
};
const RenderRow: React.FC<{ row: IFile; rowIndex: number; dirId: string }> = ({
const RenderRow: React.FC<{ row: IFile; rowIndex: number; dirId: number }> = ({
row,
rowIndex,
dirId
}) => {
const [setIconForFile] = useExplorerStore((state) => [state.setIconForFile]);
const selectedFileIndex = useSelectedFileIndex(dirId);
const [collectDir, selectedRow, setSelectedRow] = useExplorerStore((state) => [
state.collectDir,
state.selected,
state.setSelected
]);
const isActive = selectedRow?.index === rowIndex;
const isActive = selectedFileIndex === rowIndex;
const isAlternate = rowIndex % 2 == 0;
function selectFile() {
if (!row.icon_b64) renderIcon(dirId, row.uri, rowIndex);
if (selectedRow?.index == rowIndex) setSelectedRow(null);
else setSelectedRow(rowIndex, row);
if (selectedFileIndex == rowIndex) useExplorerStore.getState().clearSelectedFiles();
else useExplorerStore.getState().selectFile(dirId, row.id);
}
return useMemo(
@@ -141,7 +112,7 @@ const RenderRow: React.FC<{ row: IFile; rowIndex: number; dirId: string }> = ({
onDoubleClick={() => {
if (row.is_dir) {
invoke<DirectoryResponse>('get_files', { path: row.uri }).then((res) => {
collectDir(res.directory, res.contents);
useExplorerStore.getState().ingestDir(res.directory, res.contents);
});
}
}}
@@ -156,7 +127,7 @@ const RenderRow: React.FC<{ row: IFile; rowIndex: number; dirId: string }> = ({
className="table-body-cell px-4 py-2 flex items-center pr-2"
style={{ width: col.width }}
>
<RenderCell rowIndex={rowIndex} dirHash={dirId} colKey={col?.key} />
<RenderCell fileId={row.id} dirId={dirId} colKey={col?.key} />
</div>
))}
</div>
@@ -165,13 +136,13 @@ const RenderRow: React.FC<{ row: IFile; rowIndex: number; dirId: string }> = ({
);
};
const RenderCell: React.FC<{ colKey?: ColumnKey; dirHash?: string; rowIndex?: number }> = ({
const RenderCell: React.FC<{ colKey?: ColumnKey; dirId?: number; fileId?: number }> = ({
colKey,
rowIndex,
dirHash
fileId,
dirId
}) => {
if (!rowIndex || !colKey || !dirHash) return <></>;
const [row] = useExplorerStore((state) => [state.dirs[dirHash].children?.[rowIndex]]);
if (!fileId || !colKey || !dirId) return <></>;
const row = useFile(fileId);
if (!row) return <></>;
const value = row[colKey];
if (!value) return <></>;
@@ -180,9 +151,11 @@ const RenderCell: React.FC<{ colKey?: ColumnKey; dirHash?: string; rowIndex?: nu
case 'name':
return (
<div className="flex flex-row items-center overflow-hidden">
{!!row?.icon_b64 && (
<img src={'data:image/png;base64, ' + row.icon_b64} className="w-6 h-6 mr-2" />
)}
<div className="w-6 h-6 mr-2">
{!!row?.icon_b64 && (
<img src={'data:image/png;base64, ' + row.icon_b64} className="w-6 h-6 mr-2" />
)}
</div>
{/* {colKey == 'name' &&
(() => {
switch (row.extension.toLowerCase()) {

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { useExplorerStore } from '../../store/explorer';
import { useExplorerStore, useSelectedFile } from '../../store/explorer';
import { Transition } from '@headlessui/react';
import { IFile } from '../../types';
interface MetaItemProps {
title: string;
@@ -19,10 +20,12 @@ const MetaItem = (props: MetaItemProps) => {
const Divider = () => <div className="w-full my-1 h-[1px] bg-gray-700" />;
export const Inspector = () => {
const [selectedFile] = useExplorerStore((state) => [state.selected?.file]);
const selectedFile = useSelectedFile();
const isOpen = !!selectedFile;
const file = selectedFile;
return (
<Transition
show={isOpen}
@@ -36,14 +39,14 @@ export const Inspector = () => {
<div className="h-full w-60 absolute right-0 top-0 m-2">
<div className="flex flex-col overflow-hidden h-full rounded-lg bg-gray-700 shadow-lg select-text">
<div className="h-32 bg-gray-750 w-full flex justify-center items-center">
{!!selectedFile?.icon_b64 && (
<img src={'data:image/png;base64, ' + selectedFile.icon_b64} className=" h-24" />
{!!file?.icon_b64 && (
<img src={'data:image/png;base64, ' + file.icon_b64} className=" h-24" />
)}
</div>
<h3 className="font-bold p-3 text-base">{selectedFile?.name}</h3>
<MetaItem title="Checksum" value={selectedFile?.meta_checksum as string} />
<h3 className="font-bold p-3 text-base">{file?.name}</h3>
<MetaItem title="Checksum" value={file?.meta_checksum as string} />
<Divider />
<MetaItem title="Uri" value={selectedFile?.uri as string} />
<MetaItem title="Uri" value={file?.uri as string} />
</div>
</div>
</Transition>

View File

@@ -18,6 +18,7 @@ import { TrafficLights } from '../os/TrafficLights';
import { Button, ButtonProps, Input } from '../primative';
import { Shortcut } from '../primative/Shortcut';
import { DefaultProps } from '../primative/types';
import { appWindow } from '@tauri-apps/api/window';
export interface TopBarProps extends DefaultProps {}
export interface TopBarButtonProps extends ButtonProps {
@@ -33,12 +34,12 @@ const TopBarButton: React.FC<TopBarButtonProps> = ({ icon: Icon, ...props }) =>
<button
{...props}
className={clsx(
'mr-[1px] py-1 px-1 text-md font-medium dark:bg-gray-550 dark:hover:bg-gray-600 dark:active:bg-gray-500 rounded-md transition-colors duration-100',
'mr-[1px] py-0.5 px-0.5 text-md font-medium dark:bg-gray-550 dark:hover:bg-gray-600 dark:active:bg-gray-500 rounded-md transition-colors duration-100',
{
'rounded-r-none rounded-l-none': props.group && !props.left && !props.right,
'rounded-r-none': props.group && props.left,
'rounded-l-none': props.group && props.right,
'dark:bg-gray-550 dark:hover:bg-gray-550 dark:active:bg-gray-550': props.active
'dark:bg-gray-450 dark:hover:bg-gray-450 dark:active:bg-gray-450': props.active
},
props.className
)}
@@ -57,32 +58,38 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
className="flex flex-shrink-0 h-11 -mt-0.5 max-w items-center border-b bg-gray-100 dark:bg-gray-550 border-gray-100 dark:border-gray-900 shadow-sm"
>
<div className="mr-32 ml-1 ">
<TrafficLights className="p-1.5" />
<TrafficLights
onClose={appWindow.close}
onFullscreen={appWindow.maximize}
onMinimize={appWindow.minimize}
className="p-1.5"
/>
</div>
<TopBarButton group left icon={ChevronLeftIcon} onClick={goBack} />
<TopBarButton group right icon={ChevronRightIcon} />
<div className="w-8"></div>
<TopBarButton active group left icon={ViewListIcon} />
<TopBarButton group icon={ViewBoardsIcon} />
<TopBarButton group right icon={ViewGridIcon} />
<div className="w-8"></div>
<div className="flex mx-8 space-x-[1px]">
<TopBarButton active group left icon={ViewListIcon} />
<TopBarButton group icon={ViewBoardsIcon} />
<TopBarButton group right icon={ViewGridIcon} />
</div>
<div className="relative flex h-7">
<Input
<input
placeholder="Search"
className="placeholder-gray-600 bg-gray-50 dark:bg-gray-500 dark:border-gray-500 dark:hover:!bg-gray-550 text-xs w-32 focus:w-52 transition-all"
className="w-32 focus:w-52 text-xs p-2 rounded-md outline-none focus:ring-2 placeholder-gray-600 dark:placeholder-gray-300 bg-gray-50 dark:bg-gray-500 dark:border-gray-500 focus:ring-gray-100 dark:focus:ring-gray-600 transition-all"
/>
<div className="space-x-1 absolute top-[1px] right-1">
<Shortcut chars="⌘" />
<Shortcut chars="S" />
</div>
</div>
<div className="w-8"></div>
<TopBarButton icon={TagIcon} />
<TopBarButton icon={FolderAddIcon} />
<TopBarButton icon={CloudIcon} />
<div className="w-8"></div>
<TopBarButton icon={ArrowsLeftRight} />
<div className="flex mx-8 space-x-2">
<TopBarButton icon={TagIcon} />
<TopBarButton icon={FolderAddIcon} />
<TopBarButton icon={CloudIcon} />
<TopBarButton icon={ArrowsLeftRight} />
</div>
<div className="flex-grow"></div>
<TopBarButton className="mr-[8px]" icon={CogIcon} />
</div>

View File

@@ -26,7 +26,8 @@ interface LightProps {
const Light: React.FC<LightProps> = (props) => {
return (
<div
className={clsx('w-[12px] h-[12px] rounded-full', {
onClick={props.action}
className={clsx('w-[12px] h-[12px] rounded-full cursor-pointer', {
'bg-red-400': props.mode == 'close',
'bg-green-400': props.mode == 'fullscreen',
'bg-yellow-400': props.mode == 'minimize'

View File

@@ -0,0 +1,27 @@
import { useEffect } from 'react';
import { emit, listen, Event } from '@tauri-apps/api/event';
import { useExplorerStore } from '../store/explorer';
export interface RustEvent {
kind: string;
data: any;
}
export function useGlobalEvents() {
useEffect(() => {
listen('message', (e: Event<RustEvent>) => {
console.log({ e });
switch (e.payload?.kind) {
case 'FileTypeThumb':
useExplorerStore
.getState()
.tempInjectThumb(e.payload.data.file_id, e.payload.data.thumbnail_b64);
break;
default:
break;
}
});
}, []);
}

1
src/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'react-spline';

View File

@@ -12,21 +12,17 @@ export interface DirectoryResponse {
}
export const ExplorerScreen: React.FC<{}> = () => {
const [activeDirHash, collectDir] = useExplorerStore((state) => [
state.activeDirHash,
state.collectDir
]);
const [currentDir] = useExplorerStore((state) => [state.currentDir]);
useEffect(() => {
invoke<DirectoryResponse>('get_files', { path: '/Users/jamie/Downloads/CoolFolder' }).then(
(res) => {
console.log({ res });
collectDir(res.directory, res.contents);
}
);
invoke<DirectoryResponse>('get_files', { path: '/Users/jamie/Downloads' }).then((res) => {
console.log({ res });
useExplorerStore.getState().ingestDir(res.directory, res.contents);
invoke('get_thumbs_for_directory', { path: '/Users/jamie/Downloads' });
});
}, []);
if (!activeDirHash) return <></>;
if (currentDir === null) return <></>;
return (
<div className="relative w-full flex flex-row bg-white dark:bg-gray-900">

View File

@@ -9,15 +9,24 @@ import { Dropdown } from '../components/primative/Dropdown';
import { InputContainer } from '../components/primative/InputContainer';
import { Shortcut } from '../components/primative/Shortcut';
import { useInputState } from '../hooks/useInputState';
//@ts-ignore
// import { Spline } from 'react-spline';
// import WINDOWS_SCENE from '../assets/spline/scene.json';
export const SettingsScreen: React.FC<{}> = () => {
const fileUploader = useRef<HTMLInputElement | null>(null);
const inputState = useInputState('/Users/jamie/Downloads');
const inputState = useInputState('/Users/jamie/Downloads/CoolFolder');
return (
<div>
<div className="p-3">
<div className="px-5">
{/* <FileList files={dummyIFile} /> */}
{/* <Spline scene={WINDOWS_SCENE} /> */}
{/* <iframe
src="https://my.spline.design/windowscopy-8e92a2e9b7cb4d9237100441e8c4f688/"
width="100%"
height="100%"
></iframe> */}
<div className="flex space-x-2 mt-4">
<InputContainer
title="Quick scan directory"
@@ -25,15 +34,10 @@ export const SettingsScreen: React.FC<{}> = () => {
>
<Input {...inputState} placeholder="/users/jamie/Desktop" />
</InputContainer>
<InputContainer
title="Quick scan directory"
description="The directory for which this application will perform a detailed scan of the contents and sub directories"
>
<Input {...inputState} placeholder="/users/jamie/Desktop" />
</InputContainer>
</div>
<div className="space-x-2 flex flex-row mt-6">
<div className="space-x-2 flex flex-row mt-2">
<Button
size="sm"
variant="primary"
onClick={() => {
invoke('scan_dir', {
@@ -43,14 +47,8 @@ export const SettingsScreen: React.FC<{}> = () => {
>
Scan Now
</Button>
<Button>Cancel</Button>
</div>
<div className="flex space-x-2 mt-2">
<Button size="sm" variant="primary">
Cancel
</Button>
<Button size="sm">Cancel</Button>
</div>
<div className="space-x-2 flex flex-row mt-4">
<Toggle initialState={false} />
</div>

7
src/store/Debug.tsx Normal file
View File

@@ -0,0 +1,7 @@
import React from 'react';
import { useExplorerStore } from './explorer';
export function DebugGlobalStore() {
useExplorerStore();
return <></>;
}

View File

@@ -1,63 +0,0 @@
import create from 'zustand';
import { IDirectory, IFile } from '../types';
import produce from 'immer';
interface ExplorerStore {
dirs: Record<string, IDirectory>;
activeDirHash: string;
history: string[];
selected: null | { index: number; file: IFile };
collectDir: (dirHash: IFile, files: IFile[]) => void;
currentDir?: () => IFile[];
setSelected: (index: number | null, file?: IFile) => void;
goBack: () => void;
setIconForFile: (dirId: string, fileIndex: number, b64: string) => void;
}
export const useExplorerStore = create<ExplorerStore>((set, get) => ({
dirs: {},
activeDirHash: '',
history: [],
selected: null,
collectDir: (directory, files) => {
set((state) =>
produce(state, (draft) => {
draft.history.push(directory.meta_checksum);
draft.activeDirHash = directory.meta_checksum;
draft.dirs[directory.meta_checksum] = {
children: files,
children_count: files.length,
...directory
};
})
);
},
goBack: () => {
if (get().history.length > 1) {
set((state) =>
produce(state, (draft) => {
draft.history.pop();
draft.activeDirHash = draft.history[draft.history.length - 1];
})
);
}
},
setIconForFile: (dirId: string, fileIndex: number, b64: string) => {
set((state) =>
produce(state, (draft) => {
console.log({ dirId, fileIndex, b64: b64?.slice(1, 50) });
const dir = draft.dirs[dirId];
const file = dir.children?.[fileIndex];
if (file) {
file.icon_b64 = b64;
}
})
);
},
setSelected: (index?: number | null, file?: IFile) =>
set((state) =>
produce(state, (draft) => {
draft.selected = !index || !file ? null : { index, file };
})
)
}));

146
src/store/explorer.tsx Normal file
View File

@@ -0,0 +1,146 @@
import create from 'zustand';
import { IDirectory, IFile } from '../types';
import produce from 'immer';
interface ExplorerStore {
// storage
files: Record<number, IFile>;
// indexes
dirs: Record<number, number[]>;
// ingest
ingestDir: (dir: IFile, children: IFile[]) => void;
// selection
selectedFile: number | null;
selectedFiles: number[];
selectedFilesHistory: string[][];
selectFile: (dirId: number, fileId: number, type?: 'below' | 'above') => void;
clearSelectedFiles: () => void;
// selectAnotherFile?: (fileId: number) => void;
// selectFilesBetween?: (firstFileId: number, secondFileId: number) => void;
// explorer state
currentDir: number | null;
dirHistory: number[];
goBack?: () => void;
goForward?: () => void;
tempInjectThumb: (fileId: number, b64: string) => void;
}
export const useExplorerStore = create<ExplorerStore>((set, get) => ({
files: {},
dirs: {},
selectedFile: null,
selectedFiles: [],
selectedFilesHistory: [],
currentDir: null,
dirHistory: [],
ingestDir: (dir, children) => {
set((state) =>
produce(state, (draft) => {
draft.files[dir.id] = dir;
// extract directory index of file ids
draft.dirs[dir.id] = children.map((file) => file.id);
// save files by id
for (const index in children) {
const child = children[index];
draft.files[child.id] = child;
}
// if this dir in the history stack, remove history since then
const existingDirHistoryIndex = draft.dirHistory.findIndex((i) => i === dir.id);
if (existingDirHistoryIndex != -1) {
draft.dirHistory.splice(existingDirHistoryIndex);
}
// push onto history stack
draft.dirHistory.push(dir.id);
draft.currentDir = dir.id;
})
);
},
goBack: () => {
set((state) =>
produce(state, (draft) => {
const prevDirId = draft.dirHistory[draft.dirHistory.length - 2];
if (prevDirId == undefined) return;
draft.currentDir = prevDirId;
})
);
},
goForward: () => {
set((state) =>
produce(state, (draft) => {
const nextDirId = draft.dirHistory[draft.dirHistory.length];
if (nextDirId == undefined) return;
draft.currentDir = nextDirId;
})
);
},
selectFile: (dirId, fileId, type) => {
set((state) =>
produce(state, (draft) => {
if (!draft.files[fileId]) return;
if (!type) {
draft.selectedFile = fileId;
}
// this is the logic for up / down movement on selected file
const dirIndex = draft.dirs[dirId];
const maxIndex = dirIndex.length - 1;
const activeIndex = dirIndex.findIndex((i) => i === fileId);
switch (type) {
case 'above':
if (activeIndex - 1 < 0) draft.selectedFile = dirIndex[maxIndex];
else draft.selectedFile = dirIndex[activeIndex - 1];
break;
case 'below':
if (activeIndex + 1 > maxIndex) draft.selectedFile = dirIndex[0];
else draft.selectedFile = dirIndex[activeIndex + 1];
break;
}
})
);
},
clearSelectedFiles: () => {
set((state) =>
produce(state, (draft) => {
draft.selectedFile = null;
})
);
},
tempInjectThumb: (fileId: number, b64: string) => {
set((state) =>
produce(state, (draft) => {
if (!draft.files[fileId]) return;
draft.files[fileId].icon_b64 = b64;
})
);
}
}));
export function useSelectedFile(): null | IFile {
const [file] = useExplorerStore((state) => [state.files[state.selectedFile || -1]]);
return file;
}
export function useSelectedFileIndex(dirId: number): null | number {
return useExplorerStore((state) =>
state.dirs[dirId].findIndex((i) => i === state.files[state.selectedFile || -1]?.id)
);
}
export function useFile(fileId: number): null | IFile {
return useExplorerStore((state) => state.files[fileId || -1]);
}
export function useCurrentDir(): IDirectory | null {
return useExplorerStore((state) => {
const children = state.dirs[state.currentDir || -1].map((id) => state.files[id]);
const directory = state.files[state.currentDir || -1];
return {
...directory,
children,
children_count: children.length
};
});
}

View File

@@ -2,7 +2,7 @@ import { Encryption } from './library';
import { ImageMeta, VideoMeta } from './media';
export interface IFile {
id?: number;
id: number;
meta_checksum: string;
uri: string;
is_dir: string;
@@ -22,6 +22,11 @@ export interface IFile {
parent_id: string;
tags?: ITag[];
// this state is used to tell the renderer to look in the designated
// folder for this media type
has_native_icon?: boolean;
has_thumb?: boolean;
has_preview_media?: boolean;
icon_b64?: string;
}

View File

@@ -1,8 +1,8 @@
Arguments:
/usr/local/bin/node /usr/local/Cellar/yarn/1.22.5/libexec/bin/yarn.js add @types/heroicons -D
/usr/local/bin/node /usr/local/Cellar/yarn/1.22.5/libexec/bin/yarn.js add @types/react-spline
PATH:
/Users/jamie/Library/Android/sdk/tools:/Users/jamie/Library/Android/sdk/platform-tools:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/Library/Apple/usr/bin:/Users/jamie/.cargo/bin
/Users/jamie/Library/Android/sdk/tools:/Users/jamie/Library/Android/sdk/platform-tools:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/Library/Apple/usr/bin:/Users/jamie/Library/Android/sdk/tools:/Users/jamie/Library/Android/sdk/platform-tools:/Users/jamie/.cargo/bin
Yarn version:
1.22.5
@@ -14,7 +14,7 @@ Platform:
darwin x64
Trace:
Error: https://registry.yarnpkg.com/@types%2fheroicons: Not found
Error: https://registry.yarnpkg.com/@types%2freact-spline: Not found
at Request.params.callback [as _callback] (/usr/local/Cellar/yarn/1.22.5/libexec/lib/cli.js:66988:18)
at Request.self.callback (/usr/local/Cellar/yarn/1.22.5/libexec/lib/cli.js:140749:22)
at Request.emit (events.js:315:20)
@@ -36,8 +36,10 @@ npm manifest:
"devDependencies": {
"@tauri-apps/cli": "^1.0.0-beta.6",
"@types/babel-core": "^6.25.7",
"@types/byte-size": "^8.1.0",
"@types/react": "^17.0.18",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.3.1",
"@types/tailwindcss": "^2.2.1",
"concurrently": "^6.2.1",
"prettier": "^2.3.2",
@@ -52,22 +54,28 @@ npm manifest:
"dependencies": {
"@apollo/client": "^3.4.7",
"@headlessui/react": "^1.4.0",
"@tailwindcss/forms": "^0.3.3",
"@heroicons/react": "^1.0.4",
"@tauri-apps/api": "^1.0.0-beta.5",
"@types/pretty-bytes": "^5.2.0",
"@types/react-table": "^7.7.6",
"@vitejs/plugin-react-refresh": "^1.3.6",
"autoprefixer": "^9",
"byte-size": "^8.1.0",
"clsx": "^1.1.1",
"heroicons": "^1.0.4",
"mobx": "^6.3.3",
"mobx-state-tree": "^5.0.3",
"immer": "^9.0.6",
"phosphor-react": "^1.3.1",
"pretty-bytes": "^5.6.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-dropzone": "^11.3.4",
"react-router-dom": "^5.2.0",
"react-spline": "^1.2.1",
"react-virtualized": "^9.22.3",
"rooks": "^5.7.1",
"tailwindcss": "^2.2.16",
"vite": "^2.4.4",
"vite-tsconfig-paths": "^3.3.13"
"vite-tsconfig-paths": "^3.3.13",
"zustand": "^3.5.13"
}
}
@@ -290,6 +298,13 @@ Lockfile:
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7":
version "7.15.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a"
integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4"
@@ -337,6 +352,11 @@ Lockfile:
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.4.0.tgz#c6d424d8ab10ac925e4423d7f3cbab02c30d736a"
integrity sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw==
"@heroicons/react@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.4.tgz#11847eb2ea5510419d7ada9ff150a33af0ad0863"
integrity sha512-3kOrTmo8+Z8o6AL0rzN82MOf8J5CuxhRLFhpI8mrn+3OqekA6d5eb1GYO3EYYo1Vn6mYQSMNTzCWbEwUInb0cQ==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -395,13 +415,6 @@ Lockfile:
dependencies:
defer-to-connect "^2.0.0"
"@tailwindcss/forms@^0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.3.3.tgz#a29d22668804f3dae293dcadbef1aa6315c45b64"
integrity sha512-U8Fi/gq4mSuaLyLtFISwuDYzPB73YzgozjxOIHsK6NXgg/IWD1FLaHbFlWmurAMyy98O+ao74ksdQefsquBV1Q==
dependencies:
mini-svg-data-uri "^1.2.3"
"@tauri-apps/api@^1.0.0-beta.5":
version "1.0.0-beta.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-1.0.0-beta.5.tgz#32727b3b478f67330a79409ceaf16a4a51410fce"
@@ -491,6 +504,11 @@ Lockfile:
dependencies:
"@types/babel-types" "*"
"@types/byte-size@^8.1.0":
version "8.1.0"
resolved "https://registry.yarnpkg.com/@types/byte-size/-/byte-size-8.1.0.tgz#013a3995d1ff2d85ad27da0801029f13328bd91b"
integrity sha512-LCIlZh8vyx+I2fgRycE1D34c33QDppYY6quBYYoaOpQ1nGhJ/avSP2VlrAefVotjJxgSk6WkKo0rTcCJwGG7vA==
"@types/cacheable-request@^6.0.1":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9"
@@ -501,6 +519,11 @@ Lockfile:
"@types/node" "*"
"@types/responselike" "*"
"@types/history@*":
version "4.7.9"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.9.tgz#1cfb6d60ef3822c589f18e70f8b12f9a28ce8724"
integrity sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==
"@types/http-cache-semantics@*":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
@@ -528,6 +551,13 @@ Lockfile:
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/pretty-bytes@^5.2.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@types/pretty-bytes/-/pretty-bytes-5.2.0.tgz#857dcf4a21839e5bfb1c188dda62f986fdfa2348"
integrity sha512-dJhMFphDp6CE+OAZVyqzha9KsmgeqRMbZN4dIbMSrfObiuzfjucwKdn6zu+ttrjMwmz+Vz71/xXgHx5pO0axhA==
dependencies:
pretty-bytes "*"
"@types/prop-types@*":
version "15.7.4"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
@@ -540,6 +570,30 @@ Lockfile:
dependencies:
"@types/react" "*"
"@types/react-router-dom@^5.3.1":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.1.tgz#76700ccce6529413ec723024b71f01fc77a4a980"
integrity sha512-UvyRy73318QI83haXlaMwmklHHzV9hjl3u71MmM6wYNu0hOVk9NLTa0vGukf8zXUqnwz4O06ig876YSPpeK28A==
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react-router" "*"
"@types/react-router@*":
version "5.1.17"
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.17.tgz#087091006213b11042f39570e5cd414863693968"
integrity sha512-RNSXOyb3VyRs/EOGmjBhhGKTbnN6fHWvy5FNLzWfOWOGjgVUKqJZXfpKzLmgoU8h6Hj8mpALj/mbXQASOb92wQ==
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react-table@^7.7.6":
version "7.7.6"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.6.tgz#4899ccc46b4a1de08cc1daf120842e37e112e51e"
integrity sha512-ZMFHh1sG5AGDmhVRpz9mgGByGmBFAqnZ7QnyqGa5iAlKtcSC3vb/gul47lM0kJ1uvlawc+qN5k+++pe+GBdJ+g==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^17.0.18":
version "17.0.18"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.18.tgz#4109cbbd901be9582e5e39e3d77acd7b66bb7fbe"
@@ -892,6 +946,11 @@ Lockfile:
base64-js "^1.3.1"
ieee754 "^1.1.13"
byte-size@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-8.1.0.tgz#6353d0bc14ab7a69abcefbf11f8db0145a862cb5"
integrity sha512-FkgMTAg44I0JtEaUAvuZTtU2a2YDmBRbQxdsQNSMtLCjhG0hMcF5b1IMN9UjSCJaU4nvlj/GER7B9sI4nKdCgA==
bytes@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
@@ -1093,7 +1152,7 @@ Lockfile:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
clsx@^1.1.1:
clsx@^1.0.4, clsx@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
@@ -1473,6 +1532,14 @@ Lockfile:
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
dom-helpers@^5.1.3:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
dependencies:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
dot-prop@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@@ -2065,11 +2132,6 @@ Lockfile:
dependencies:
function-bind "^1.1.1"
heroicons@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/heroicons/-/heroicons-1.0.4.tgz#58a9c5fafae7185fca79f8b47251484903e4dc0a"
integrity sha512-dn1OvwZruCNEmNDr+TRvG5MejL9kW4VtVQO2Y1yfp7rz0+yWqpkjwwFRTQ8VHBF26oNMMUofQYA+Jl363Y7JdA==
hex-color-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
@@ -2179,6 +2241,11 @@ Lockfile:
p-pipe "^4.0.0"
replace-ext "^2.0.0"
immer@^9.0.6:
version "9.0.6"
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==
import-cwd@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92"
@@ -2564,6 +2631,11 @@ Lockfile:
pinkie-promise "^2.0.0"
strip-bom "^2.0.0"
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.topath@^4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009"
@@ -2731,11 +2803,6 @@ Lockfile:
"@babel/runtime" "^7.12.1"
tiny-warning "^1.0.3"
mini-svg-data-uri@^1.2.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.3.3.tgz#91d2c09f45e056e5e1043340b8b37ba7b50f4fac"
integrity sha512-+fA2oRcR1dJI/7ITmeQJDrYWks0wodlOz0pAEhKYJ2IVc1z0AnwJUsKY2fzFmPAM3Jo9J0rBx8JAA9QQSJ5PuA==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -2753,16 +2820,6 @@ Lockfile:
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mobx-state-tree@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-5.0.3.tgz#f026a26be39ba4ef71e4256b9f27d6176a3b2072"
integrity sha512-68VKZb30bLWN6MFyYvpvwM93VMEqyJLJsbjTDrYmbRAK594n5LJqQXa7N3N00L/WjcujzMtqkT7CcbM0zqTIcA==
mobx@^6.3.3:
version "6.3.3"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.3.tgz#a3006c56243b1c7ea4ee671a66f963b9f43cf1af"
integrity sha512-JoNU50rO6d1wHwKPJqKq4rmUMbYnI9CsJmBo+Cu4exBYenFvIN77LWrZENpzW6reZPADtXMmB1DicbDSfy8Clw==
modern-normalize@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-1.1.0.tgz#da8e80140d9221426bd4f725c6e11283d34f90b7"
@@ -3141,6 +3198,11 @@ Lockfile:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
phosphor-react@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/phosphor-react/-/phosphor-react-1.3.1.tgz#96e33f44370d83cda15b60cccc17087ad0060684"
@@ -3291,6 +3353,11 @@ Lockfile:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d"
integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==
pretty-bytes@*, pretty-bytes@^5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
pretty-hrtime@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
@@ -3364,6 +3431,13 @@ Lockfile:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
dependencies:
performance-now "^2.1.0"
rc@^1.2.7, rc@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@@ -3397,6 +3471,11 @@ Lockfile:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-refresh@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.10.0.tgz#2f536c9660c0b9b1d500684d9e52a65e7404f7e3"
@@ -3431,6 +3510,25 @@ Lockfile:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-spline@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/react-spline/-/react-spline-1.2.1.tgz#c074178cf752b1f582fb62af0c330d9a3f5e0099"
integrity sha512-d17gOvoKjWfHmsXm7H1YbK715dDEVuIc72eNcrrpNp7T/H8zH2yfNvk1NSJQSuGchTXaa90hbiGRC3FGpJHMZA==
dependencies:
three "0.123.0"
react-virtualized@^9.22.3:
version "9.22.3"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.3.tgz#f430f16beb0a42db420dbd4d340403c0de334421"
integrity sha512-MKovKMxWTcwPSxE1kK1HcheQTWfuCxAuBoSTf2gwyMM21NdX/PXUhnoP8Uc5dRKd+nKm8v41R36OellhdCpkrw==
dependencies:
"@babel/runtime" "^7.7.2"
clsx "^1.0.4"
dom-helpers "^5.1.3"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-lifecycles-compat "^3.0.4"
react@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
@@ -3656,6 +3754,14 @@ Lockfile:
optionalDependencies:
fsevents "~2.3.2"
rooks@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/rooks/-/rooks-5.7.1.tgz#a9c91e94e760e21a9df5afc1de4ba07d09a6156d"
integrity sha512-Gztycgm+e+bS0vqLMSGlGe8f7rkXMxjfPj3FucM06/xu1CEFQx1pZ0zMVdWVxDeMXRePaQ2/g1K7ArIlGKyHbQ==
dependencies:
lodash.debounce "^4.0.8"
raf "^3.4.1"
run-async@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
@@ -4147,6 +4253,11 @@ Lockfile:
temp-dir "^1.0.0"
uuid "^3.0.1"
three@0.123.0:
version "0.123.0"
resolved "https://registry.yarnpkg.com/three/-/three-0.123.0.tgz#3bb6d8f908a432eb7cd450f7eab6dd40fde53085"
integrity sha512-KNnx/IbilvoHRkxOtL0ouozoDoElyuvAXhFB21RK7F5IPWSmqyFelICK6x3hJerLNSlAdHxR0hkuvMMhH9pqXg==
through@^2.3.6, through@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -4537,3 +4648,8 @@ Lockfile:
bin-build "^3.0.0"
bin-wrapper "^4.0.1"
logalot "^2.1.0"
zustand@^3.5.13:
version "3.5.13"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.5.13.tgz#fd1af8c14a23ec7efdeb9ab167b8e69a8fc0980a"
integrity sha512-orO/XcYwSWffsrPVTdCtuKM/zkUaOIyKDasOk/lecsD3R0euELsj+cB65uKZ1KyinrK2STHIuUhRoLpH8QprQg==

View File

@@ -3425,6 +3425,13 @@ react-router@5.2.0:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-spline@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/react-spline/-/react-spline-1.2.1.tgz#c074178cf752b1f582fb62af0c330d9a3f5e0099"
integrity sha512-d17gOvoKjWfHmsXm7H1YbK715dDEVuIc72eNcrrpNp7T/H8zH2yfNvk1NSJQSuGchTXaa90hbiGRC3FGpJHMZA==
dependencies:
three "0.123.0"
react-virtualized@^9.22.3:
version "9.22.3"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.3.tgz#f430f16beb0a42db420dbd4d340403c0de334421"
@@ -4161,6 +4168,11 @@ tempfile@^2.0.0:
temp-dir "^1.0.0"
uuid "^3.0.1"
three@0.123.0:
version "0.123.0"
resolved "https://registry.yarnpkg.com/three/-/three-0.123.0.tgz#3bb6d8f908a432eb7cd450f7eab6dd40fde53085"
integrity sha512-KNnx/IbilvoHRkxOtL0ouozoDoElyuvAXhFB21RK7F5IPWSmqyFelICK6x3hJerLNSlAdHxR0hkuvMMhH9pqXg==
through@^2.3.6, through@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"