mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 08:53:32 +00:00
Rust+Swift bridge
Co-authored-by: Brendonovich <brendonovich@outlook.com>
This commit is contained in:
parent
eb64c90f83
commit
600adc8fa9
19
.TODO
19
.TODO
|
@ -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..
|
||||
|
|
|
@ -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
3
src-tauri/Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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" ]
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
28
src-tauri/src/swift/Package.swift
Normal file
28
src-tauri/src/swift/Package.swift
Normal 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
7
src-tauri/swift/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
30
src-tauri/swift/Package.swift
Normal file
30
src-tauri/swift/Package.swift
Normal 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")
|
||||
]
|
||||
)
|
3
src-tauri/swift/README.md
Normal file
3
src-tauri/swift/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# spacedive_swift
|
||||
|
||||
A description of this package.
|
62
src-tauri/swift/src/spacedrive.swift
Normal file
62
src-tauri/swift/src/spacedrive.swift
Normal 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
|
||||
}
|
|
@ -53,8 +53,8 @@
|
|||
"windows": [
|
||||
{
|
||||
"title": "SpaceDrive",
|
||||
"width": 1200,
|
||||
"height": 700,
|
||||
"width": 1100,
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"alwaysOnTop": false,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
47
src/components/file/Inspector.tsx
Normal file
47
src/components/file/Inspector.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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" /> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
})
|
||||
)
|
||||
}));
|
||||
|
|
|
@ -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] }
|
||||
|
|
34
yarn.lock
34
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue