Rust+Swift bridge

Co-authored-by: Brendonovich <brendonovich@outlook.com>
This commit is contained in:
Jamie 2021-10-19 07:31:56 -07:00
parent eb64c90f83
commit 600adc8fa9
22 changed files with 520 additions and 76 deletions

19
.TODO
View file

@ -7,6 +7,21 @@
✔ App frame and router set up
✔ Primative UI components (Button, Input, Shortcut etc)
✔ Render basic file list with database data
☐ Create a global store
✔ Create a global store
☐ File inspector <- CURRENT
☐ Tag creation and assignment
☐ Generate buffer hash
☐ Right click menu
☐ Settings screen
☐ Set up Tauri updater
☐ Volume identification
☐ Job queue system
☐ Native file previews
☐ Secret keystore
☐ File encryptor
☐ File viewer / player
☐ Open with
☐ Explorer grid view
☐ Explorer path viewer / editor
☐ Statistic calucations
Folder size, Volume size, Total capacity, Total unique etc..

View file

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

3
src-tauri/Cargo.lock generated
View file

@ -10,6 +10,7 @@ dependencies = [
"bytesize",
"cargo-edit",
"chrono",
"cocoa",
"crossbeam",
"data-encoding",
"env_logger",
@ -18,6 +19,8 @@ dependencies = [
"libc",
"log",
"nix 0.23.0",
"objc",
"objc-foundation",
"once_cell",
"rebind",
"refinery",

View file

@ -11,6 +11,8 @@ build = "src/build.rs"
[build-dependencies]
tauri-build = { version = "1.0.0-beta.3" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dependencies]
tauri = { version = "1.0.0-beta.5", features = ["api-all", "menu"] }
@ -40,6 +42,9 @@ bytesize = "1.1.0"
libc = "0.2.103"
nix = "0.23.0"
log = "0.4.14"
objc = "0.2.7"
cocoa = "0.24.0"
objc-foundation = "0.1.1"
[features]
default = [ "custom-protocol" ]

View file

@ -1,3 +1,86 @@
fn main() {
tauri_build::build()
use serde::Deserialize;
use serde_json;
use std::env;
use std::process::Command;
const MACOS_TARGET_VERSION: &str = "11";
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SwiftTargetInfo {
triple: String,
unversioned_triple: String,
module_triple: String,
swift_runtime_compatibility_version: String,
#[serde(rename = "librariesRequireRPath")]
libraries_require_rpath: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SwiftPaths {
runtime_library_paths: Vec<String>,
runtime_library_import_paths: Vec<String>,
runtime_resource_path: String,
}
#[derive(Debug, Deserialize)]
struct SwiftTarget {
target: SwiftTargetInfo,
paths: SwiftPaths,
}
/// Builds mac_ddc library Swift project, sets the library search options right so we link
/// against Swift run-time correctly.
fn build_swift_natives() {
let profile = env::var("PROFILE").unwrap();
let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let target = format!("{}-apple-macosx{}", arch, MACOS_TARGET_VERSION);
let swift_target_info_str = Command::new("swift")
.args(&["-target", &target, "-print-target-info"])
.output()
.unwrap()
.stdout;
let swift_target_info: SwiftTarget = serde_json::from_slice(&swift_target_info_str).unwrap();
if swift_target_info.target.libraries_require_rpath {
panic!("Libraries require RPath! Change minimum MacOS value to fix.")
}
if !Command::new("swift")
.args(&["build", "-c", &profile])
.current_dir("./swift")
.status()
.unwrap()
.success()
{
panic!("Swift natives compilation failed")
}
swift_target_info
.paths
.runtime_library_paths
.iter()
.for_each(|path| {
println!("cargo:rustc-link-search=native={}", path);
});
println!(
"cargo:rustc-link-search=native=./swift/.build/{}/{}",
swift_target_info.target.unversioned_triple, profile
);
println!("cargo:rustc-link-lib=static=spacedrive");
println!("cargo:rerun-if-changed=swift/src/*.swift");
println!(
"cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={}",
MACOS_TARGET_VERSION
);
}
fn main() {
// let target = env::var("CARGO_CFG_TARGET_OS").unwrap();
// if target == "macos" {
build_swift_natives();
// }
tauri_build::build();
}

View file

@ -39,7 +39,7 @@ pub async fn init_library() -> Result<()> {
println!("created library {:?}", &library);
} else {
println!("library loaded {:?}", library.unwrap());
// println!("library loaded {:?}", library.unwrap());
};
Ok(())
}

View file

@ -14,13 +14,76 @@ use env_logger;
use futures::executor::block_on;
// use systemstat::{saturating_sub_bytes, Platform, System};
#[derive(Debug)]
#[repr(C)]
struct FFIString {
// _phantom: u8,
data: *const u8,
length: u64,
}
#[derive(Debug)]
#[repr(C)]
struct FFIData {
// _phantom: u8,
data: *const u8,
length: u64,
}
extern "C" {
fn get_file_thumbnail(ptr: *const u8, length: u64) -> FFIData;
fn test(ptr: *const u8, length: u64) -> FFIData;
}
fn main() {
let path = "/Users/jamie/Downloads/Audio Hijack.app";
// let string = FFIString {
// data: path.as_ptr(),
// length: path.len(),
// };
// let thumbnail = unsafe {
// println!("{:?}", string);
// get_file_thumbnail(&string)
// };
println!("Struct Data: {:?}", unsafe {
let res = test(path.as_ptr(), path.len() as u64);
let mut vec = Vec::<u8>::new();
let mut pointer_str = String::new();
for j in 8..16 {
let num = *(res.data.add(j));
vec.push(num);
pointer_str = format!("{:x}", num) + &pointer_str;
}
let pointer_num = u64::from_str_radix(&pointer_str, 16).unwrap();
let pointer = pointer_num as *const u8;
let mut length = 0;
for j in 16..24 {
let num = *(res.data.add(j));
length += num as usize * 256_usize.pow(j as u32 - 16_u32)
}
let mut data = Vec::new();
for i in 0..length {
data.push(*pointer.add(i));
}
res
});
// unsafe { get_icon("/Users/jamie/Downloads/Audio Hijack.app") }
// let mounts = device::volumes_c::get_mounts();
// println!("mounted drives: {:?}", &mounts);
env_logger::builder()
.filter_level(log::LevelFilter::Debug)
.is_test(true)
.init();
// env_logger::builder()
// .filter_level(log::LevelFilter::Debug)
// .is_test(true)
// .init();
// create primary data base if not exists
block_on(db::connection::create_primary_db()).unwrap();

View file

@ -0,0 +1,28 @@
// 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: "drive_mac",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "swift",
targets: ["swift"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
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: "swift",
dependencies: []),
.testTarget(
name: "swiftTests",
dependencies: ["swift"]),
]
)

7
src-tauri/swift/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

View file

@ -0,0 +1,30 @@
// 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: "spacedrive",
platforms: [
.macOS(.v11),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "spacedrive",
type: .static,
targets: ["spacedrive"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
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: "spacedrive",
dependencies: [],
path: "src")
]
)

View file

@ -0,0 +1,3 @@
# spacedive_swift
A description of this package.

View file

@ -0,0 +1,62 @@
import Foundation
import AppKit
public class FFIString: NSObject {
var data: UnsafePointer<CChar>
var length: UInt64
init(data: UnsafePointer<CChar>, length: UInt64) {
self.data = data
self.length = length
}
}
public class FFIData: NSObject {
var data: UnsafePointer<UInt8>
var length: UInt64
init(data: UnsafePointer<UInt8>, length: UInt64) {
self.data = data
self.length = length
}
}
@_cdecl("get_file_thumbnail")
public func getFileThumbnail(path_ptr: UnsafePointer<CChar>, path_length: UInt64) -> FFIData {
print(path_length)
let path = String(data: Data(bytes: path_ptr, count: Int(path_length)), encoding: String.Encoding.utf8)!
print(path)
let image = NSWorkspace.shared.icon(forFile: path)
let bitmap = NSBitmapImageRep(data: image.tiffRepresentation!)!.representation(using: .png, properties: [:])!
let pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmap.count)
bitmap.copyBytes(to: pointer, count: bitmap.count)
return FFIData(data: UnsafePointer(pointer), length: UInt64(bitmap.count))
}
@_cdecl("test")
public func test(path_ptr: UnsafePointer<CChar>, path_length: UInt64) -> FFIString {
let path = String(data: Data(bytes: path_ptr, count: Int(path_length)), encoding: String.Encoding.utf8)!
print(path)
let image = NSWorkspace.shared.icon(forFile: path)
let bitmap = NSBitmapImageRep(data: image.tiffRepresentation!)!.representation(using: .png, properties: [:])!
let pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmap.count)
bitmap.copyBytes(to: pointer, count: bitmap.count)
let data = UnsafePointer<UInt8>(pointer);
let length = UInt64(bitmap.count)
print(data)
print(length)
let ret = FFIString(
data: UnsafePointer<CChar>(strdup(path)!),
length: UInt64(path.lengthOfBytes(using: .utf8))
)
print(ret)
return ret
}

View file

@ -53,8 +53,8 @@
"windows": [
{
"title": "SpaceDrive",
"width": 1200,
"height": 700,
"width": 1100,
"height": 600,
"resizable": true,
"fullscreen": false,
"alwaysOnTop": false,

View file

@ -1,5 +1,5 @@
import React, { useRef } from 'react';
import { Route, BrowserRouter as Router, Switch } from 'react-router-dom';
import { Route, BrowserRouter as Router, Switch, Redirect } from 'react-router-dom';
import { Sidebar } from './components/file/Sidebar';
import { TopBar } from './components/layout/TopBar';
import { useInputState } from './hooks/useInputState';
@ -9,12 +9,15 @@ import { ExplorerScreen } from './screens/Explorer';
export default function App() {
return (
<Router>
<div className="flex flex-col h-screen rounded-xl border border-gray-200 dark:border-gray-600 bg-white text-gray-900 dark:text-white dark:bg-gray-800 overflow-hidden ">
<div className="flex flex-col select-none h-screen rounded-xl border border-gray-200 dark:border-gray-600 bg-white text-gray-900 dark:text-white dark:bg-gray-800 overflow-hidden ">
<TopBar />
<div className="flex flex-row min-h-full">
<Sidebar />
<div className="w-full flex bg-gray-50 dark:bg-gray-800">
<div className="relative w-full flex bg-gray-50 dark:bg-gray-800">
<Switch>
<Route exact path="/">
<Redirect to="/explorer" />
</Route>
<Route path="/settings">
<SettingsScreen />
</Route>

View file

@ -1,6 +1,6 @@
import { DocumentIcon, DotsVerticalIcon, FilmIcon, FolderIcon } from '@heroicons/react/solid';
import clsx from 'clsx';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { IFile } from '../../types';
import byteSize from 'pretty-bytes';
import { useKey } from 'rooks';
@ -22,13 +22,16 @@ function ensureIsColumns<T extends Column[]>(data: T) {
const columns = ensureIsColumns([
{ column: 'Name', key: 'name', width: 280 } as const,
{ column: 'Size', key: 'size_in_bytes', width: 120 } as const,
{ column: 'Checksum', key: 'meta_checksum', width: 120 } as const
{ column: 'Type', key: 'extension', width: 100 } as const
// { column: 'Checksum', key: 'meta_checksum', width: 120 } as const
// { column: 'Tags', key: 'tags', width: 120 } as const
]);
type ColumnKey = typeof columns[number]['key'];
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) => [
@ -42,18 +45,35 @@ export const FileList: React.FC<{}> = (props) => {
useKey('ArrowUp', (e) => {
e.preventDefault();
if (selectedRow > 1) setSelectedRow(selectedRow - 1);
else setSelectedRow(currentDir.children_count);
if (!selectedRow || !currentDir?.children) return;
if (selectedRow?.index > 0)
// decrement selected index
setSelectedRow(selectedRow.index - 1, currentDir.children[selectedRow.index - 1]);
// loop to bottom
else setSelectedRow(currentDir.children_count, currentDir.children[currentDir.children_count]);
});
useKey('ArrowDown', (e) => {
e.preventDefault();
if (selectedRow < currentDir.children_count) setSelectedRow(selectedRow + 1);
else setSelectedRow(0);
if (!selectedRow || !currentDir?.children) return;
// increment if rows below exist
if (selectedRow.index < currentDir.children_count)
setSelectedRow(selectedRow.index + 1, currentDir.children[selectedRow.index + 1]);
else setSelectedRow(0, currentDir.children[0]);
});
function isRowOutOfView(rowHeight: number, rowIndex: number) {
const scrollTop = scrollContainer.current?.scrollTop || 0;
}
function handleScroll() {}
return useMemo(
() => (
<div className="table-container w-full h-full overflow-scroll bg-white dark:bg-gray-900 p-3 ">
<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) => (
@ -68,7 +88,7 @@ export const FileList: React.FC<{}> = (props) => {
))}
</div>
</div>
<div className="table-body">
<div className="table-body pb-10">
{currentDir?.children?.map((row, index) => (
<RenderRow key={row.id} row={row} rowIndex={index} />
))}
@ -86,13 +106,18 @@ const RenderRow: React.FC<{ row: IFile; rowIndex: number }> = ({ row, rowIndex }
state.setSelected
]);
const isActive = selectedRow === row.id;
const isActive = selectedRow?.index === rowIndex;
const isAlternate = rowIndex % 2 == 0;
function selectFile() {
if (selectedRow?.index == rowIndex) setSelectedRow(null);
else setSelectedRow(rowIndex, row);
}
return useMemo(
() => (
<div
onClick={() => setSelectedRow(row.id as number)}
onClick={selectFile}
onDoubleClick={() => {
if (row.is_dir) {
invoke<DirectoryResponse>('get_files', { path: row.uri }).then((res) => {
@ -106,7 +131,11 @@ const RenderRow: React.FC<{ row: IFile; rowIndex: number }> = ({ row, rowIndex }
})}
>
{columns.map((col) => (
<div key={col.key} className="table-body-cell px-4 py-2" style={{ width: col.width }}>
<div
key={col.key}
className="table-body-cell px-4 py-2 flex items-center pr-2"
style={{ width: col.width }}
>
<RenderCell row={row} colKey={col?.key} />
</div>
))}
@ -123,25 +152,28 @@ const RenderCell: React.FC<{ colKey?: ColumnKey; row?: IFile }> = ({ colKey, row
switch (colKey) {
case 'name':
return (
<div className="flex flex-row items-center">
<div className="flex flex-row items-center overflow-hidden">
{colKey == 'name' &&
(() => {
switch (row.extension.toLowerCase()) {
case 'mov' || 'mp4':
return <FilmIcon className="w-5 h-5 mr-3 flex-shrink-0" />;
return <FilmIcon className="w-5 h-5 mr-3 flex-shrink-0 text-gray-300" />;
default:
if (row.is_dir) return <FolderIcon className="w-5 h-5 mr-3 flex-shrink-0" />;
return <DocumentIcon className="w-5 h-5 mr-3 flex-shrink-0" />;
if (row.is_dir)
return <FolderIcon className="w-5 h-5 mr-3 flex-shrink-0 text-gray-300" />;
return <DocumentIcon className="w-5 h-5 mr-3 flex-shrink-0 text-gray-300" />;
}
})()}
<span className="truncate">{row[colKey]}</span>
<span className="truncate text-xs">{row[colKey]}</span>
</div>
);
case 'size_in_bytes':
return <span>{byteSize(Number(value || 0))}</span>;
case 'meta_checksum':
return <span className="truncate">{value}</span>;
return <span className="text-xs text-left">{byteSize(Number(value || 0))}</span>;
case 'extension':
return <span className="text-xs text-left">{value.toLowerCase()}</span>;
// case 'meta_checksum':
// return <span className="truncate">{value}</span>;
// case 'tags':
// return renderCellWithIcon(MusicNoteIcon);

View file

@ -0,0 +1,47 @@
import React from 'react';
import { useExplorerStore } from '../../store/explorer';
import { Transition } from '@headlessui/react';
interface MetaItemProps {
title: string;
value: string;
}
const MetaItem = (props: MetaItemProps) => {
return (
<div className="meta-item flex flex-col p-3">
<h5 className="font-bold text-sm">{props.title}</h5>
<p className="break-all text-xs text-gray-300">{props.value}</p>
</div>
);
};
const Divider = () => <div className="w-full my-1 h-[1px] bg-gray-700" />;
export const Inspector = () => {
const [selectedFile] = useExplorerStore((state) => [state.selected?.file]);
const isOpen = !!selectedFile;
return (
<Transition
show={isOpen}
enter="transition-translate ease-in-out duration-200"
enterFrom="translate-x-64"
enterTo="translate-x-0"
leave="transition-translate ease-in-out duration-200"
leaveFrom="translate-x-0"
leaveTo="translate-x-64"
>
<div className="h-full w-52 absolute right-0 top-0 m-1">
<div className="flex flex-col overflow-hidden h-full rounded-md bg-gray-800 shadow-xl select-text">
<div className="h-32 bg-gray-950 w-full" />
<h3 className="font-bold p-3 text-base">{selectedFile?.name}</h3>
<MetaItem title="Checksum" value={selectedFile?.meta_checksum as string} />
<Divider />
<MetaItem title="Uri" value={selectedFile?.uri as string} />
</div>
</div>
</Transition>
);
};

View file

@ -13,22 +13,22 @@ import { Dropdown } from '../primative/Dropdown';
import { DefaultProps } from '../primative/types';
const tabs = [
{ name: 'Spaces', icon: Planet, uri: '/spaces' },
{ name: 'Photos', icon: PhotographIcon, uri: '/photos' },
{ name: 'Storage', icon: ServerIcon, uri: '/storage' },
// { name: 'Spaces', icon: Planet, uri: '/spaces' },
{ name: 'Explorer', icon: CubeTransparentIcon, uri: '/explorer' },
{ name: 'Settings', icon: CogIcon, uri: '/settings' }
{ name: 'Photos', icon: PhotographIcon, uri: '/photos' },
{ name: 'Storage', icon: ServerIcon, uri: '/settings' }
// { name: 'Settings', icon: CogIcon, uri: '/settings' }
];
interface SidebarProps extends DefaultProps {}
export const Sidebar: React.FC<SidebarProps> = (props) => {
return (
<div className="w-46 flex flex-col flex-wrap flex-shrink-0 min-h-full bg-gray-50 dark:bg-gray-800 border-gray-100 border-r dark:border-gray-700 px-3 space-y-0.5">
<div className="w-46 flex flex-col flex-wrap flex-shrink-0 min-h-full bg-gray-50 dark:bg-gray-850 border-gray-100 border-r dark:border-gray-700 px-3 space-y-0.5">
<Dropdown
buttonProps={{
justifyLeft: true,
className: 'mb-1 flex-shrink-0 w-[175px]',
className: 'mb-1 shadow flex-shrink-0 w-[175px] dark:bg-gray-650',
variant: 'gray'
}}
buttonText="Jamie's Library"
@ -38,7 +38,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
{tabs.map((button, index) => (
<NavLink
key={index}
className="max-w rounded px-2 py-1 flex flex-row items-center hover:bg-gray-200 dark:hover:bg-gray-700 text-sm"
className="max-w rounded px-2 py-1 flex flex-row items-center hover:bg-gray-200 dark:hover:bg-gray-600 text-sm"
activeClassName="bg-gray-200 hover:bg-gray-200 dark:bg-gray-500 dark:hover:bg-gray-500"
to={button.uri}
>

View file

@ -7,46 +7,67 @@ import {
ViewGridIcon,
ViewListIcon
} from '@heroicons/react/outline';
import clsx from 'clsx';
import { HouseSimple } from 'phosphor-react';
import React from 'react';
import { useExplorerStore } from '../../store/explorer';
import { TrafficLights } from '../os/TrafficLights';
import { Button, Input } from '../primative';
import { Button, ButtonProps, Input } from '../primative';
import { Shortcut } from '../primative/Shortcut';
import { DefaultProps } from '../primative/types';
export interface TopBarProps extends DefaultProps {}
export interface TopBarButtonProps extends ButtonProps {
icon: any;
group?: boolean;
active?: boolean;
left?: boolean;
right?: boolean;
}
const TopBarButton: React.FC<TopBarButtonProps> = ({ icon: Icon, ...props }) => {
return (
<button
{...props}
className={clsx(
'mr-[1px] py-1 px-1 text-md font-medium dark:bg-gray-650 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
},
props.className
)}
>
<Icon className="m-0.5 w-4 h-4 dark:text-white" />
</button>
);
};
export const TopBar: React.FC<TopBarProps> = (props) => {
const [goBack] = useExplorerStore((state) => [state.goBack]);
return (
<>
<div
data-tauri-drag-region
className="flex flex-shrink-0 h-10 max-w items-center bg-gray-100 dark:bg-gray-800 border-gray-100 dark:border-gray-900 shadow-sm "
className="flex flex-shrink-0 h-10 max-w items-center border-b bg-gray-100 dark:bg-gray-650 border-gray-100 dark:border-gray-900 shadow-sm "
>
<div className="mr-32 ml-1 ">
<TrafficLights className="p-1.5" />
</div>
<Button noBorder noPadding className="rounded-r-none mr-[1px]">
<ChevronLeftIcon className="m-0.5 w-4 h-4 dark:text-white" />
</Button>
<Button noBorder noPadding className="rounded-l-none">
<ChevronRightIcon className="m-0.5 w-4 h-4 dark:text-white" />
</Button>
<TopBarButton group left icon={ChevronLeftIcon} onClick={goBack} />
<TopBarButton group right icon={ChevronRightIcon} />
<div className="w-4"></div>
<Button variant="selected" noBorder noPadding className="rounded-r-none mr-[1px]">
<ViewListIcon className="m-0.5 w-4 h-4 dark:text-white" />
</Button>
<Button noBorder noPadding className="rounded-none mr-[1px]">
<ViewBoardsIcon className="m-0.5 w-4 h-4 dark:text-white" />
</Button>
<Button noBorder noPadding className="rounded-l-none">
<ViewGridIcon className="m-0.5 w-4 h-4 dark:text-white" />
</Button>
<TopBarButton active group left icon={ViewListIcon} />
<TopBarButton group icon={ViewBoardsIcon} />
<TopBarButton group right icon={ViewGridIcon} />
<div className="w-4"></div>
<div className="relative flex h-7">
<Input
placeholder="Search"
className="placeholder-gray-600 bg-gray-50 text-xs w-32 focus:w-52 transition-all"
className="placeholder-gray-600 bg-gray-50 zdark:bg-gray-600 dark:hover:bg-gray-600 text-xs w-32 focus:w-52 transition-all"
/>
<div className="space-x-1 absolute top-[1px] right-1">
<Shortcut chars="⌘" />
@ -54,11 +75,9 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
</div>
</div>
<div className="flex-grow"></div>
<Button noBorder noPadding className="mr-2">
<CogIcon className="m-0.5 w-4 h-4 dark:text-white" />
</Button>
<TopBarButton className="mr-[8px]" icon={CogIcon} />
</div>
<div className="h-[1px] flex-shrink-0 max-w bg-gray-200 dark:bg-gray-700" />
{/* <div className="h-[1px] flex-shrink-0 max-w bg-gray-200 dark:bg-gray-700" /> */}
</>
);
};

View file

@ -4,6 +4,7 @@ import { emit, listen } from '@tauri-apps/api/event';
import { invoke } from '@tauri-apps/api';
import { IFile } from '../types';
import { useExplorerStore } from '../store/explorer';
import { Inspector } from '../components/file/Inspector';
export interface DirectoryResponse {
directory: IFile;
@ -26,8 +27,9 @@ export const ExplorerScreen: React.FC<{}> = () => {
if (!activeDirHash) return <></>;
return (
<div className="w-full">
<div className="relative w-full flex flex-row bg-white dark:bg-gray-900">
<FileList />
<Inspector />
</div>
);
};

View file

@ -6,17 +6,18 @@ interface ExplorerStore {
dirs: Record<string, IDirectory>;
activeDirHash: string;
history: string[];
selected: number;
selected: null | { index: number; file: IFile };
collectDir: (dirHash: IFile, files: IFile[]) => void;
currentDir?: () => IFile[];
setSelected: (id: number) => void;
setSelected: (index: number | null, file?: IFile) => void;
goBack: () => void;
}
export const useExplorerStore = create<ExplorerStore>((set, get) => ({
dirs: {},
activeDirHash: '',
history: [],
selected: 0,
selected: null,
collectDir: (directory, files) => {
set((state) =>
produce(state, (draft) => {
@ -40,10 +41,10 @@ export const useExplorerStore = create<ExplorerStore>((set, get) => ({
);
}
},
setSelected: (id) =>
setSelected: (index?: number | null, file?: IFile) =>
set((state) =>
produce(state, (draft) => {
draft.selected = id;
draft.selected = !index || !file ? null : { index, file };
})
)
}));

View file

@ -33,15 +33,23 @@ module.exports = {
DEFAULT: '#505468',
50: '#F1F1F4',
100: '#E8E9ED',
200: '#E0E1E6',
150: '#E0E1E6',
200: '#D8DAE3',
250: '#D2D4DC',
300: '#C0C2CE',
400: '#6F7590',
350: '#A6AABF',
400: '#9196A8',
450: '#71758A',
500: '#505468',
600: '#434656',
700: '#353845',
800: '#282A34',
850: '#21212B',
900: '#1B1C23'
550: '#434656',
600: '#3E414F',
650: '#353845',
700: '#333745',
750: '#282A34',
800: '#262832',
850: '#30303E',
900: '#22242F',
950: '#15161D'
}
}
// fontFamily: { sans: ['Inter', ...defaultTheme.fontFamily.sans] }

View file

@ -213,6 +213,13 @@
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"
@ -1060,7 +1067,7 @@ clone@^1.0.2:
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==
@ -1440,6 +1447,14 @@ dlv@^1.1.3:
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"
@ -3371,6 +3386,11 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
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"
@ -3405,6 +3425,18 @@ react-router@5.2.0:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.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"