Merge pull request #431 from spacedriveapp/spacedrive-but-themable

New color system & better UI components
This commit is contained in:
Oscar Beaumont 2022-10-25 12:41:00 +10:00 committed by GitHub
commit 4493f15065
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
125 changed files with 12960 additions and 1754 deletions

View file

@ -14,6 +14,7 @@ Flac
haden
haoyuan
haris
Iconoir
josephjacks
justinhoffman
Keychain

View file

@ -35,5 +35,11 @@
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"rust-analyzer.procMacro.enable": true,
"rust-analyzer.diagnostics.experimental.enable": false
"rust-analyzer.diagnostics.experimental.enable": false,
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
"tw`([^`]*)",
"tw\\.[^`]+`([^`]*)`",
"tw\\(.*?\\).*?`([^`]*)"
],
}

46
Cargo.lock generated
View file

@ -1974,7 +1974,7 @@ checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64"
dependencies = [
"anyhow",
"heck 0.4.0",
"proc-macro-crate 1.2.1",
"proc-macro-crate",
"proc-macro-error",
"proc-macro2",
"quote",
@ -2080,7 +2080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9"
dependencies = [
"anyhow",
"proc-macro-crate 1.2.1",
"proc-macro-crate",
"proc-macro-error",
"proc-macro2",
"quote",
@ -2535,36 +2535,13 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "int-enum"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b1428b2b1abe959e6eedb0a17d0ab12f6ba20e1106cc29fc4874e3ba393c177"
dependencies = [
"cfg-if 0.1.10",
"int-enum-impl 0.4.0",
]
[[package]]
name = "int-enum"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff87d3cc4b79b4559e3c75068d64247284aceb6a038bd4bb38387f3f164476d"
dependencies = [
"int-enum-impl 0.5.0",
]
[[package]]
name = "int-enum-impl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c3cecaad8ca1a5020843500c696de2b9a07b63b624ddeef91f85f9bafb3671"
dependencies = [
"cfg-if 0.1.10",
"proc-macro-crate 0.1.5",
"proc-macro2",
"quote",
"syn",
"int-enum-impl",
]
[[package]]
@ -2573,7 +2550,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1f2f068675add1a3fc77f5f5ab2e29290c841ee34d151abc007bce902e5d34"
dependencies = [
"proc-macro-crate 1.2.1",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
@ -3480,7 +3457,7 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce"
dependencies = [
"proc-macro-crate 1.2.1",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
@ -4224,15 +4201,6 @@ dependencies = [
"uuid 0.8.2",
]
[[package]]
name = "proc-macro-crate"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
dependencies = [
"toml",
]
[[package]]
name = "proc-macro-crate"
version = "1.2.1"
@ -5178,7 +5146,7 @@ dependencies = [
"hostname 0.3.1",
"image",
"include_dir",
"int-enum 0.4.0",
"int-enum",
"itertools",
"normi",
"once_cell",
@ -5255,7 +5223,7 @@ dependencies = [
name = "sd-file-ext"
version = "0.0.0"
dependencies = [
"int-enum 0.5.0",
"int-enum",
"serde",
"serde_json",
]

View file

@ -8,7 +8,8 @@
"vite": "vite",
"dev": "tauri dev",
"tauri": "tauri",
"build": "tauri build"
"build": "tauri build",
"dmg": "open ../../target/release/bundle/dmg/"
},
"dependencies": {
"@rspc/tauri": "^0.0.0-main-7c0a67c1",
@ -22,6 +23,7 @@
},
"devDependencies": {
"@tauri-apps/cli": "1.1.1",
"@tauri-apps/tauricon": "github:tauri-apps/tauricon",
"@types/babel-core": "^6.25.7",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",

View file

@ -59,9 +59,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
#[cfg(target_os = "macos")]
{
use macos::{lock_app_theme, AppThemeType};
// use macos::{lock_app_theme, AppThemeType};
lock_app_theme(AppThemeType::Dark as _);
// lock_app_theme(AppThemeType::Dark as _);
}
app.windows().iter().for_each(|(_, window)| {
@ -83,7 +83,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
Ok(())
})
.on_menu_event(menu::handle_menu_event)
.invoke_handler(tauri::generate_handler![app_ready,])
.invoke_handler(tauri::generate_handler![app_ready])
.menu(menu::get_menu())
.build(tauri::generate_context!())?;

View file

@ -65,7 +65,7 @@
"title": "Spacedrive",
"width": 1400,
"height": 725,
"minWidth": 700,
"minWidth": 768,
"minHeight": 500,
"resizable": true,
"fullscreen": false,

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="vanilla-theme">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />

View file

@ -1,6 +1,6 @@
import { loggerLink } from '@rspc/client';
import { loggerLink, splitLink } from '@rspc/client';
import { tauriLink } from '@rspc/tauri';
import { OperatingSystem, PlatformProvider, hooks, queryClient } from '@sd/client';
import { OperatingSystem, PlatformProvider, getDebugState, hooks, queryClient } from '@sd/client';
import SpacedriveInterface, { Platform } from '@sd/interface';
import { KeybindEvent } from '@sd/interface';
import { dialog, invoke, os, shell } from '@tauri-apps/api';
@ -10,9 +10,14 @@ import { createRoot } from 'react-dom/client';
import '@sd/ui/style';
const isDev = import.meta.env.DEV;
const client = hooks.createClient({
links: [...(isDev ? [loggerLink()] : []), tauriLink()]
links: [
splitLink({
condition: () => getDebugState().rspcLogger,
true: [loggerLink(), tauriLink()],
false: [tauriLink()]
})
]
});
async function getOs(): Promise<OperatingSystem> {
@ -33,7 +38,9 @@ const platform: Platform = {
getThumbnailUrlById: (casId) => `spacedrive://thumbnail/${encodeURIComponent(casId)}`,
openLink: shell.open,
getOs,
openFilePickerDialog: () => dialog.open({ directory: true })
openFilePickerDialog: () => dialog.open({ directory: true }),
showDevtools: () => invoke('show_devtools'),
openPath: (path) => shell.open(path)
};
function App() {

View file

@ -1,9 +1,7 @@
import { Disclosure, Transition } from '@headlessui/react';
import { ChevronRightIcon, XMarkIcon } from '@heroicons/react/24/solid';
import { ChevronRightIcon } from '@heroicons/react/24/solid';
import { Button } from '@sd/ui';
import clsx from 'clsx';
import { List, X } from 'phosphor-react';
import { PropsWithChildren, useEffect, useState } from 'react';
import { PropsWithChildren, useState } from 'react';
import pkg from 'react-burger-menu';
import { Doc, DocsNavigation, toTitleCase } from '../pages/docs/api';
@ -32,9 +30,10 @@ export default function DocsLayout(props: Props) {
<div className="visible h-screen pb-20 overflow-x-hidden custom-scroll doc-sidebar-scroll bg-gray-650 pt-7 px-7 sm:invisible">
<Button
onClick={() => setMenuOpen(!menuOpen)}
icon={<X weight="bold" className="w-6 h-6" />}
className="!px-1 -ml-0.5 mb-3 !border-none"
/>
>
<X weight="bold" className="w-6 h-6" />
</Button>
<DocsSidebar activePath={props?.doc?.url} navigation={props.navigation} />
</div>
</Menu>
@ -45,11 +44,9 @@ export default function DocsLayout(props: Props) {
<div className="flex flex-col w-full sm:flex-row" id="page-container">
<div className="h-12 px-5 flex w-full border-t border-gray-600 border-b mt-[65px] sm:hidden items-center ">
<div className="flex sm:hidden">
<Button
onClick={() => setMenuOpen(!menuOpen)}
icon={<List weight="bold" className="w-6 h-6" />}
className="!px-2 ml-1 !border-none"
/>
<Button onClick={() => setMenuOpen(!menuOpen)} className="!px-2 ml-1 !border-none">
<List weight="bold" className="w-6 h-6" />
</Button>
</div>
{props.doc?.url.split('/').map((item, index) => {
if (index === 2) return null;

View file

@ -88,7 +88,7 @@ export function HomeCTA() {
<Button
onClick={() => setShowWaitlistInput(true)}
className="z-30 border-0 cursor-pointer"
variant="primary"
variant="gray"
>
Join Waitlist
</Button>
@ -96,7 +96,7 @@ export function HomeCTA() {
href="https://github.com/spacedriveapp/spacedrive"
target="_blank"
className="z-30 cursor-pointer"
variant="gray"
variant="accent"
>
<Github className="inline w-5 h-5 -mt-[4px] -ml-1 mr-2" fill="white" />
Star on GitHub
@ -115,9 +115,9 @@ export function HomeCTA() {
})}
>
{waitlistError ? (
<Alert className="fill-red-500 w-5 mr-1" />
<Alert className="w-5 mr-1 fill-red-500" />
) : (
<Info className="fill-green-500 w-5 mr-1" />
<Info className="w-5 mr-1 fill-green-500" />
)}
<p
className={clsx({
@ -150,7 +150,7 @@ export function HomeCTA() {
'opacity-50 cursor-default': loading
})}
disabled={loading}
variant="primary"
variant="accent"
type="submit"
>
{loading ? (

View file

@ -7,12 +7,11 @@ import {
} from '@heroicons/react/24/solid';
import { Discord, Github } from '@icons-pack/react-simple-icons';
import AppLogo from '@sd/assets/images/logo.png';
import { Dropdown, DropdownItem } from '@sd/ui';
import { Button, Dropdown } from '@sd/ui';
import clsx from 'clsx';
import { DotsThreeVertical } from 'phosphor-react';
import { PropsWithChildren, useEffect, useState } from 'react';
import * as router from 'vite-plugin-ssr/client/router';
import { positions } from '../pages/careers.page';
import { getWindow } from '../utils';
@ -30,23 +29,15 @@ function NavLink(props: PropsWithChildren<{ link?: string }>) {
);
}
function dropdownItem(
props: { name: string; icon: any } & ({ href: string } | { path: string })
): DropdownItem[number] {
if ('href' in props) {
return {
name: props.name,
icon: props.icon,
onPress: () => (window.location.href = props.href)
};
} else {
return {
name: props.name,
icon: props.icon,
onPress: () => (window.location.href = props.path),
selected: getWindow()?.location.href.includes(props.path)
};
}
function link(path: string) {
return {
selected: getWindow()?.location.href.includes(path),
onClick: () => router.navigate(path)
};
}
function redirect(href: string) {
return () => (window.location.href = href);
}
export default function NavBar() {
@ -90,60 +81,53 @@ export default function NavBar() {
<NavLink link="/careers">Careers</NavLink>
{positions.length > 0 ? (
<span className="absolute bg-opacity-80 px-[5px] text-xs rounded-md bg-primary -top-1 -right-2">
{' '}
{positions.length}{' '}
{` ${positions.length} `}
</span>
) : null}
</div>
</div>
<Dropdown
className="absolute block h-6 text-white w-44 top-2 right-4 lg:hidden"
itemsClassName="!rounded-2xl shadow-2xl shadow-black p-2 !bg-gray-850 mt-2 !border-gray-500"
itemButtonClassName="!py-1 !rounded-md text-[15px]"
items={[
[
dropdownItem({
name: 'Repository',
icon: Github,
href: 'https://github.com/spacedriveapp/spacedrive'
}),
dropdownItem({
name: 'Join Discord',
icon: Discord,
href: 'https://discord.gg/gTaF2Z44f5'
})
],
[
dropdownItem({
name: 'Roadmap',
icon: MapIcon,
path: '/roadmap'
}),
dropdownItem({
name: 'Docs',
icon: BookOpenIcon,
path: '/docs/product/getting-started/introduction'
}),
dropdownItem({
name: 'Team',
icon: UsersIcon,
path: '/team'
}),
dropdownItem({
name: 'Blog',
icon: ChatBubbleOvalLeftIcon,
path: '/blog'
}),
dropdownItem({
name: 'Careers',
icon: AcademicCapIcon,
path: '/careers'
})
]
]}
buttonIcon={<DotsThreeVertical weight="bold" className="w-6 h-6 " />}
buttonProps={{ className: '!p-1 ml-[140px] hover:!bg-transparent' }}
/>
<div className="flex-1 lg:hidden" />
<Dropdown.Root
button={
<Button className="ml-[140px] hover:!bg-transparent" size="icon">
<DotsThreeVertical weight="bold" className="w-6 h-6 " />
</Button>
}
className="block h-6 text-white w-44 top-2 right-4 lg:hidden"
itemsClassName="!rounded-2xl shadow-2xl shadow-black p-2 !bg-gray-850 mt-2 !border-gray-500 text-[15px]"
>
<Dropdown.Section>
<Dropdown.Item
icon={Github}
onClick={redirect('https://github.com/spacedriveapp/spacedrive')}
>
Repository
</Dropdown.Item>
<Dropdown.Item icon={Discord} onClick={redirect('https://discord.gg/gTaF2Z44f5')}>
Join Discord
</Dropdown.Item>
</Dropdown.Section>
<Dropdown.Section>
<Dropdown.Item icon={MapIcon} {...link('/roadmap')}>
Roadmap
</Dropdown.Item>
<Dropdown.Item
icon={BookOpenIcon}
{...link('/docs/product/getting-started/introduction')}
>
Docs
</Dropdown.Item>
<Dropdown.Item icon={UsersIcon} {...link('/team')}>
Team
</Dropdown.Item>
<Dropdown.Item icon={ChatBubbleOvalLeftIcon} {...link('/blog')}>
Blog
</Dropdown.Item>
<Dropdown.Item icon={AcademicCapIcon} {...link('/careers')}>
Careers
</Dropdown.Item>
</Dropdown.Section>
</Dropdown.Root>
<div className="absolute flex-row hidden space-x-5 right-3 lg:flex">
<a href="https://discord.gg/gTaF2Z44f5" target="_blank" rel="noreferrer">

View file

@ -110,7 +110,7 @@ function Page() {
<Button
onClick={scrollToPositions}
className="z-30 border-0 cursor-pointer"
variant="primary"
variant="accent"
>
See Open Positions
</Button>

View file

@ -3,7 +3,6 @@ import config from './docs';
export async function onBeforeRender() {
const navigation = getDocsNavigation(config);
return {
pageContext: {
pageProps: {

View file

@ -6,6 +6,12 @@ import type { PageContext } from './types';
export { render };
// Enable Client Routing
export const clientRouting = true;
// See `Link prefetching` section below. Default value: `{ when: 'HOVER' }`.
export const prefetchStaticAssets = { when: 'HOVER' };
async function render(pageContext: PageContextBuiltInClient & PageContext) {
const { Page, pageProps } = pageContext;
hydrateRoot(
@ -15,5 +21,3 @@ async function render(pageContext: PageContextBuiltInClient & PageContext) {
</App>
);
}
export const clientRouting = true;

View file

@ -27,7 +27,7 @@ function Page({ is404 }: { is404: boolean }) {
>
Back
</Button>
<Button href="/" className="mt-2 cursor-pointer !text-white" variant="primary">
<Button href="/" className="mt-2 cursor-pointer !text-white" variant="accent">
Discover Spacedrive
</Button>
</div>

View file

@ -169,7 +169,7 @@ html {
}
.slot-block {
@apply bg-gray-550 py-3 px-4 border-l-4 border-gray-400 rounded mb-2;
@apply bg-app-box py-3 px-4 border-l-4 border-app-line rounded mb-2;
}
.slot-block.note {
@apply border-yellow-400 bg-yellow-300/20;
@ -237,6 +237,6 @@ html {
@apply bg-[#00000006] dark:bg-[#00000030] my-[10px] rounded-[6px];
}
&::-webkit-scrollbar-thumb {
@apply rounded-[6px] bg-gray-300 dark:bg-gray-550;
@apply rounded-[6px] bg-app-selected
}
}

8715
apps/mobile/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -16,8 +16,10 @@ type FolderProps = {
const FolderIcon: React.FC<FolderProps> = ({ size = 24, isWhite, ...svgProps }) => {
return isWhite ? (
// @ts-expect-error
<FolderWhite width={size} height={size} {...svgProps} />
) : (
// @ts-expect-error
<Folder width={size} height={size} {...svgProps} />
);
};

View file

@ -4,7 +4,7 @@
export type Procedures = {
queries:
{ key: "files.readMetadata", input: LibraryArgs<number>, result: null } |
{ key: "getNode", input: never, result: NodeState } |
{ key: "nodeState", input: never, result: NodeState } |
{ key: "jobs.getHistory", input: LibraryArgs<null>, result: Array<JobReport> } |
{ key: "jobs.getRunning", input: LibraryArgs<null>, result: Array<JobReport> } |
{ key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } |

View file

@ -1,5 +1,5 @@
import { createWSClient, loggerLink, wsLink } from '@rspc/client';
import { PlatformProvider, hooks, queryClient } from '@sd/client';
import { createWSClient, loggerLink, splitLink, wsLink } from '@rspc/client';
import { PlatformProvider, getDebugState, hooks, queryClient } from '@sd/client';
import SpacedriveInterface, { Platform } from '@sd/interface';
import { useEffect } from 'react';
@ -7,12 +7,16 @@ const wsClient = createWSClient({
url: import.meta.env.VITE_SDSERVER_BASE_URL || 'ws://localhost:8080/rspc/ws'
});
const isDev = import.meta.env.DEV && false; // TODO: Remove false
const ws = wsLink({
client: wsClient
});
const client = hooks.createClient({
links: [
...(isDev ? [loggerLink()] : []),
wsLink({
client: wsClient
splitLink({
condition: () => getDebugState().rspcLogger,
true: [loggerLink(), ws],
false: [ws]
})
]
});

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html class="dark">
<html class="vanilla-theme">
<head>
<meta charset="utf-8" />
<title>Spacedrive</title>

View file

@ -29,7 +29,7 @@ serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4.22", features = ["serde"] }
serde_json = "1.0"
futures = "0.3"
int-enum = "0.4.0"
int-enum = "0.5.0"
rmp = "^0.8.11"
rmp-serde = "^1.1.1"
blake3 = "1.3.1"

11
core/build.rs Normal file
View file

@ -0,0 +1,11 @@
use std::process::Command;
fn main() {
let output = Command::new("git")
.args(&["rev-parse", "--short", "HEAD"])
.output()
.expect("error getting git hash. Does `git rev-parse --short HEAD` work for you?");
let git_hash = String::from_utf8(output.stdout)
.expect("Error passing output of `git rev-parse --short HEAD`");
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
}

View file

@ -0,0 +1,40 @@
/*
Warnings:
- You are about to drop the column `checksum` on the `key` table. All the data in the column will be lost.
- You are about to alter the column `algorithm` on the `key` table. The data in that column could be lost. The data in that column will be cast from `Int` to `Binary`.
- Added the required column `content_salt` to the `key` table without a default value. This is not possible if the table is not empty.
- Added the required column `default` to the `key` table without a default value. This is not possible if the table is not empty.
- Added the required column `hashing_algorithm` to the `key` table without a default value. This is not possible if the table is not empty.
- Added the required column `key` to the `key` table without a default value. This is not possible if the table is not empty.
- Added the required column `key_nonce` to the `key` table without a default value. This is not possible if the table is not empty.
- Added the required column `master_key` to the `key` table without a default value. This is not possible if the table is not empty.
- Added the required column `master_key_nonce` to the `key` table without a default value. This is not possible if the table is not empty.
- Added the required column `salt` to the `key` table without a default value. This is not possible if the table is not empty.
- Added the required column `uuid` to the `key` table without a default value. This is not possible if the table is not empty.
- Made the column `algorithm` on table `key` required. This step will fail if there are existing NULL values in that column.
*/
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_key" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"uuid" TEXT NOT NULL,
"name" TEXT,
"default" BOOLEAN NOT NULL,
"date_created" DATETIME DEFAULT CURRENT_TIMESTAMP,
"algorithm" BLOB NOT NULL,
"hashing_algorithm" BLOB NOT NULL,
"salt" BLOB NOT NULL,
"content_salt" BLOB NOT NULL,
"master_key" BLOB NOT NULL,
"master_key_nonce" BLOB NOT NULL,
"key_nonce" BLOB NOT NULL,
"key" BLOB NOT NULL
);
INSERT INTO "new_key" ("algorithm", "date_created", "id", "name") SELECT "algorithm", "date_created", "id", "name" FROM "key";
DROP TABLE "key";
ALTER TABLE "new_key" RENAME TO "key";
CREATE UNIQUE INDEX "key_uuid_key" ON "key"("uuid");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View file

@ -20,6 +20,9 @@ pub(crate) fn mount() -> RouterBuilder {
.library_query("getRunning", |t| {
t(|ctx, _: (), _| async move { Ok(ctx.jobs.get_running().await) })
})
.library_query("isRunning", |t| {
t(|ctx, _: (), _| async move { Ok(ctx.jobs.get_running().await.len() > 0) })
})
.library_query("getHistory", |t| {
t(|_, _: (), library| async move { Ok(JobManager::get_history(&library).await?) })
})

View file

@ -60,8 +60,19 @@ pub(crate) fn mount() -> Arc<Router> {
let r = <Router>::new()
.config(config)
.query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION")))
.query("getNode", |t| {
.query("buildInfo", |t| {
#[derive(Serialize, Type)]
pub struct BuildInfo {
version: &'static str,
commit: &'static str,
}
t(|_, _: ()| BuildInfo {
version: env!("CARGO_PKG_VERSION"),
commit: env!("GIT_HASH"),
})
})
.query("nodeState", |t| {
t(|ctx, _: ()| async move {
Ok(NodeState {
config: ctx.config.get().await,

View file

@ -112,6 +112,7 @@ impl Worker {
}
drop(worker);
invalidate_query!(ctx, "jobs.isRunning");
// spawn task to handle receiving events from the worker
let library_ctx = ctx.clone();
tokio::spawn(Worker::track_progress(
@ -234,6 +235,7 @@ impl Worker {
error!("failed to update job report: {:#?}", e);
}
invalidate_query!(library, "jobs.isRunning");
invalidate_query!(library, "jobs.getRunning");
invalidate_query!(library, "jobs.getHistory");

View file

@ -3,9 +3,10 @@ use crate::{
library::LibraryContext,
prisma::{file_path, location, object},
};
use chrono::{DateTime, FixedOffset};
use int_enum::IntEnum;
use prisma_client_rust::{prisma_models::PrismaValue, raw::Raw, Direction};
use sd_file_ext::{extensions::Extension, kind::ObjectKind};
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
@ -131,8 +132,8 @@ impl StatefulJob for FileIdentifierJob {
) -> Result<(), JobError> {
let db = ctx.library_ctx().db;
// link file_path ids to a CreateFile struct containing unique file data
let mut chunk: HashMap<i32, CreateFile> = HashMap::new();
// link file_path ids to a CreateObject struct containing unique file data
let mut chunk: HashMap<i32, CreateObject> = HashMap::new();
let mut cas_lookup: HashMap<String, i32> = HashMap::new();
let data = state
@ -222,6 +223,7 @@ impl StatefulJob for FileIdentifierJob {
PrismaValue::String(object.cas_id.clone()),
PrismaValue::Int(object.size_in_bytes),
PrismaValue::DateTime(object.date_created),
PrismaValue::Int(object.kind.int_value() as i64),
]);
}
@ -230,9 +232,9 @@ impl StatefulJob for FileIdentifierJob {
let created_files: Vec<FileCreated> = db
._query_raw(Raw::new(
&format!(
"INSERT INTO object (cas_id, size_in_bytes, date_created) VALUES {}
"INSERT INTO object (cas_id, size_in_bytes, date_created, kind) VALUES {}
ON CONFLICT (cas_id) DO NOTHING RETURNING id, cas_id",
vec!["({}, {}, {})"; new_objects.len()].join(",")
vec!["({}, {}, {}, {})"; new_objects.len()].join(",")
),
values,
))
@ -360,10 +362,11 @@ async fn get_orphan_file_paths(
}
#[derive(Deserialize, Serialize, Debug)]
struct CreateFile {
struct CreateObject {
pub cas_id: String,
pub size_in_bytes: i64,
pub date_created: DateTime<FixedOffset>,
pub kind: ObjectKind,
}
#[derive(Deserialize, Serialize, Debug)]
@ -375,7 +378,7 @@ struct FileCreated {
async fn assemble_object_metadata(
location_path: impl AsRef<Path>,
file_path: &file_path::Data,
) -> Result<CreateFile, io::Error> {
) -> Result<CreateObject, io::Error> {
let path = location_path
.as_ref()
.join(file_path.materialized_path.as_str());
@ -384,7 +387,19 @@ async fn assemble_object_metadata(
let metadata = fs::metadata(&path).await?;
// let date_created: DateTime<Utc> = metadata.created().unwrap().into();
// derive Object kind
let object_kind: ObjectKind = match path.extension() {
Some(ext) => match ext.to_str() {
Some(ext) => {
let mut file = std::fs::File::open(&path).unwrap();
let resolved_ext = Extension::resolve_conflicting(ext, &mut file, true);
resolved_ext.map(Into::into).unwrap_or(ObjectKind::Unknown)
}
None => ObjectKind::Unknown,
},
None => ObjectKind::Unknown,
};
let size = metadata.len();
@ -398,9 +413,10 @@ async fn assemble_object_metadata(
}
};
Ok(CreateFile {
Ok(CreateObject {
cas_id,
size_in_bytes: size as i64,
date_created: file_path.date_created,
kind: object_kind,
})
}

View file

@ -3,8 +3,8 @@
use int_enum::IntEnum;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, Copy, IntEnum)]
#[repr(u8)]
#[repr(i32)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, IntEnum)]
pub enum ObjectKind {
// A file that can not be identified by the indexer
Unknown = 0,

View file

@ -157,13 +157,11 @@ pub fn verify_magic_bytes<T: MagicBytes>(ext: T, file: &mut std::fs::File) -> Op
use std::io::{Read, Seek, SeekFrom};
for magic in ext.magic_bytes_meta() {
println!("magic: {:?}", magic);
let mut buf = vec![0; magic.length];
file.seek(SeekFrom::Start(magic.offset as u64)).ok()?;
file.read_exact(&mut buf).ok()?;
println!("buf: {:?}", buf);
if ext.has_magic_bytes(&buf) {
return Some(ext);
}

73
crates/sync/docs/HLC.md Normal file
View file

@ -0,0 +1,73 @@
```rust
pub fn update_with_timestamp(&self, timestamp: &Timestamp) -> Result<(), String> {
let mut now = (self.clock)();
now.0 &= LMASK;
let msg_time = timestamp.get_time();
if *msg_time > now && *msg_time - now > self.delta {
let err_msg = format!(
"incoming timestamp from {} exceeding delta {}ms is rejected: {} vs. now: {}",
timestamp.get_id(),
self.delta.to_duration().as_millis(),
msg_time,
now
);
warn!("{}", err_msg);
Err(err_msg)
} else {
let mut last_time = lock!(self.last_time);
let max_time = cmp::max(cmp::max(now, *msg_time), *last_time);
if max_time == now {
*last_time = now;
} else if max_time == *msg_time {
*last_time = *msg_time + 1;
} else {
*last_time += 1;
}
Ok(())
}
}
```
```javascript
Timestamp.recv = function (msg) {
if (!clock) {
return null;
}
var now = Date.now();
var msg_time = msg.millis();
var msg_time = msg.counter();
if (msg_time - now > config.maxDrift) {
throw new Timestamp.ClockDriftError();
}
var last_time = clock.timestamp.millis();
var last_time = clock.timestamp.counter();
var max_time = Math.max(Math.max(last_time, now), msg_time);
var last_time =
max_time === last_time && lNew === msg_time
? Math.max(last_time, msg_time) + 1
: max_time === last_time
? last_time + 1
: max_time === msg_time
? msg_time + 1
: 0;
// 3.
if (max_time - phys > config.maxDrift) {
throw new Timestamp.ClockDriftError();
}
if (last_time > MAX_COUNTER) {
throw new Timestamp.OverflowError();
}
clock.timestamp.setMillis(max_time);
clock.timestamp.setCounter(last_time);
return new Timestamp(clock.timestamp.millis(), clock.timestamp.counter(), clock.timestamp.node());
};
```

View file

@ -85,4 +85,4 @@ Used for TagOnFile and FileInSpace.
Indicates that a relation field should be set to the current node.
This could be done manually,
but `@node` allows `node_id` fields to be resolved from the `node_id` field of a `CRDTOperation`,
saving on bandwidth
saving on bandwidth

View file

@ -12,7 +12,7 @@
"desktop": "pnpm --filter @sd/desktop --",
"web": "pnpm --filter @sd/web -- ",
"mobile": "pnpm --filter @sd/mobile --",
"server": "pnpm --filter @sd/server -- ",
"core": "pnpm --filter @sd/server -- ",
"landing": "pnpm --filter @sd/landing -- ",
"ui": "pnpm --filter @sd/ui -- ",
"interface": "pnpm --filter @sd/interface -- ",

View file

@ -0,0 +1,4 @@
<svg width="29" height="18" viewBox="0 0 29 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.214203 3.80705L4.02126 0L17.9805 13.9592L14.1734 17.7663L0.214203 3.80705Z" fill="#D9D9D9"/>
<path d="M28.1356 3.80705L24.3286 0L10.3694 13.9592L14.1764 17.7663L28.1356 3.80705Z" fill="#D9D9D9"/>
</svg>

After

Width:  |  Height:  |  Size: 311 B

View file

@ -0,0 +1,3 @@
<svg width="537" height="144" viewBox="0 0 537 144" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M339.5 72C339.5 111.8 307.3 144 267.5 144C227.7 144 195.5 111.8 195.5 72C195.5 32.2 227.7 0 267.5 0C307.3 0 339.5 32.2 339.5 72ZM464.5 0C424.7 0 392.5 32.2 392.5 72C392.5 111.8 424.7 144 464.5 144C504.3 144 536.5 111.8 536.5 72C536.5 32.2 504.3 0 464.5 0ZM72.5 0C32.7 0 0.5 32.2 0.5 72C0.5 111.8 32.7 144 72.5 144C112.3 144 144.5 111.8 144.5 72C144.5 32.2 112.3 0 72.5 0Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 511 B

View file

@ -18,7 +18,7 @@
"@rspc/react": "^0.0.0-main-7c0a67c1",
"@sd/config": "workspace:*",
"@tanstack/react-query": "^4.12.0",
"valtio": "^1.7.0",
"valtio": "^1.7.4",
"valtio-persist": "^1.0.2"
},
"devDependencies": {

View file

@ -11,6 +11,8 @@ export type Platform = {
demoMode?: boolean; // TODO: Remove this in favour of demo mode being handled at the React Query level
getOs?(): Promise<OperatingSystem>;
openFilePickerDialog?(): Promise<null | string | string[]>;
showDevtools?(): void;
openPath?(path: string): void;
};
// Keep this private and use through helpers below

View file

@ -3,10 +3,11 @@
export type Procedures = {
queries:
{ key: "buildInfo", input: never, result: BuildInfo } |
{ key: "files.readMetadata", input: LibraryArgs<number>, result: null } |
{ key: "getNode", input: never, result: NodeState } |
{ key: "jobs.getHistory", input: LibraryArgs<null>, result: Array<JobReport> } |
{ key: "jobs.getRunning", input: LibraryArgs<null>, result: Array<JobReport> } |
{ key: "jobs.isRunning", input: LibraryArgs<null>, result: boolean } |
{ key: "library.getStatistics", input: LibraryArgs<null>, result: Statistics } |
{ key: "library.list", input: never, result: Array<LibraryConfigWrapped> } |
{ key: "locations.getById", input: LibraryArgs<number>, result: Location | null } |
@ -14,6 +15,7 @@ export type Procedures = {
{ key: "locations.indexer_rules.get", input: LibraryArgs<number>, result: IndexerRule } |
{ key: "locations.indexer_rules.list", input: LibraryArgs<null>, result: Array<IndexerRule> } |
{ key: "locations.list", input: LibraryArgs<null>, result: Array<{ id: number, pub_id: Array<number>, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } |
{ key: "nodeState", input: never, result: NodeState } |
{ key: "normi.composite", input: never, result: NormalisedCompositeId } |
{ key: "normi.org", input: never, result: NormalisedOrganisation } |
{ key: "normi.user", input: never, result: NormalisedUser } |
@ -23,7 +25,6 @@ export type Procedures = {
{ key: "tags.getExplorerData", input: LibraryArgs<number>, result: ExplorerData } |
{ key: "tags.getForObject", input: LibraryArgs<number>, result: Array<Tag> } |
{ key: "tags.list", input: LibraryArgs<null>, result: Array<Tag> } |
{ key: "version", input: never, result: string } |
{ key: "volumes.list", input: never, result: Array<Volume> },
mutations:
{ key: "files.delete", input: LibraryArgs<number>, result: null } |
@ -51,6 +52,8 @@ export type Procedures = {
{ key: "jobs.newThumbnail", input: LibraryArgs<null>, result: string }
};
export interface BuildInfo { version: string, commit: string }
export interface ConfigMetadata { version: string | null }
export interface EditLibraryArgs { id: string, name: string | null, description: string | null }

View file

@ -6,10 +6,14 @@ import { getExplorerStore, useBridgeQuery } from '../index';
// The name of the localStorage key for caching library data
const libraryCacheLocalStorageKey = 'sd-library-list';
const activeLibraryLocalStorageKey = 'sd-active-library';
type OnNoLibraryFunc = () => void | Promise<void>;
// Keep this private and use `useCurrentLibrary` hook to access or mutate it
const currentLibraryUuidStore = proxy({ id: null as string | null });
const currentLibraryUuidStore = proxy({
id: localStorage.getItem(activeLibraryLocalStorageKey) as string | null
});
const CringeContext = createContext<{
onNoLibrary: OnNoLibraryFunc;
@ -56,7 +60,7 @@ export const useCurrentLibrary = () => {
localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(data));
// Redirect to the onboarding flow if the user doesn't have any libraries
if (libraries?.length === 0) {
if (data?.length === 0) {
ctx.onNoLibrary();
}
}
@ -64,6 +68,7 @@ export const useCurrentLibrary = () => {
const switchLibrary = useCallback((libraryUuid: string) => {
currentLibraryUuidStore.id = libraryUuid;
localStorage.setItem(activeLibraryLocalStorageKey, libraryUuid);
getExplorerStore().reset();
}, []);

View file

@ -0,0 +1,23 @@
/// <reference types="vite/client" />
import { useSnapshot } from 'valtio';
import { valtioPersist } from '.';
export const debugState = valtioPersist('sd-debugState', {
// @ts-ignore
enabled: import.meta.env.DEV,
rspcLogger: false,
// @ts-ignore
reactQueryDevtools: (import.meta.env.DEV ? 'invisible' : 'enabled') as
| 'enabled'
| 'disabled'
| 'invisible'
});
export function useDebugState() {
return useSnapshot(debugState);
}
export function getDebugState() {
return debugState;
}

View file

@ -16,9 +16,11 @@ const state = {
gridItemSize: 100,
listItemSize: 40,
selectedRowIndex: 1,
tagAssignMode: false,
showInspector: true,
multiSelectIndexes: [] as number[],
contextMenuObjectId: null as number | null,
contextMenuActiveObject: null as Object | null,
newThumbnails: {} as Record<string, boolean>
};

View file

@ -1 +1,3 @@
export * from './explorerStore';
export * from './debugState';
export * from './util';

View file

@ -0,0 +1,32 @@
import { proxy, useSnapshot } from 'valtio';
import proxyWithPersist, { PersistStrategy, ProxyPersistStorageEngine } from 'valtio-persist';
const storage: ProxyPersistStorageEngine = {
getItem: (name) => window.localStorage.getItem(name),
setItem: (name, value) => window.localStorage.setItem(name, value),
removeItem: (name) => window.localStorage.removeItem(name),
getAllKeys: () => Object.keys(window.localStorage)
};
const appThemeStore = proxyWithPersist({
// must be unique, files/paths will be created with this prefix
name: 'appTheme',
version: 0,
initialState: {
themeName: 'vanilla',
themeMode: 'light' as 'light' | 'dark',
syncThemeWithSystem: false,
hueValue: null as number | null
},
persistStrategies: PersistStrategy.SingleFile,
migrations: {},
getStorage: () => storage
});
export function useThemeStore() {
return useSnapshot(appThemeStore);
}
export function getThemeStore() {
return appThemeStore;
}

View file

@ -1,3 +1,4 @@
import { proxy, subscribe } from 'valtio';
import { ProxyPersistStorageEngine } from 'valtio-persist';
export function resetStore<T extends Record<string, any>, E extends Record<string, any>>(
@ -16,3 +17,11 @@ export const storageEngine: ProxyPersistStorageEngine = {
removeItem: (name) => window.localStorage.removeItem(name),
getAllKeys: () => Object.keys(window.localStorage)
};
// The `valtio-persist` library is not working so this is a small alternative for us to use.
export function valtioPersist<T extends object>(localStorageKey: string, initialObject?: T): T {
const d = localStorage.getItem(localStorageKey);
const p = proxy(d !== null ? JSON.parse(d) : initialObject);
subscribe(p, () => localStorage.setItem(localStorageKey, JSON.stringify(p)));
return p;
}

View file

@ -26,6 +26,8 @@
"@sd/assets": "workspace:*",
"@sd/client": "workspace:*",
"@sd/ui": "workspace:*",
"@splinetool/react-spline": "^2.2.3",
"@splinetool/runtime": "^0.9.128",
"@tailwindcss/forms": "^0.5.3",
"@tanstack/react-query": "^4.12.0",
"@tanstack/react-query-devtools": "^4.12.0",
@ -35,6 +37,8 @@
"byte-size": "^8.1.0",
"clsx": "^1.2.1",
"dayjs": "^1.11.5",
"iconoir": "^5.3.2",
"iconoir-react": "^5.3.2",
"phosphor-react": "^1.4.1",
"react": "^18.2.0",
"react-colorful": "^5.6.1",
@ -49,7 +53,7 @@
"tailwindcss": "^3.1.8",
"use-count-up": "^3.0.1",
"use-debounce": "^8.0.4",
"valtio": "^1.7.0"
"valtio": "^1.7.4"
},
"devDependencies": {
"@sd/config": "workspace:*",

View file

@ -1,5 +1,5 @@
import '@fontsource/inter/variable.css';
import { LibraryContextProvider, queryClient } from '@sd/client';
import { LibraryContextProvider, queryClient, useDebugState } from '@sd/client';
import { QueryClientProvider, defaultContext } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import dayjs from 'dayjs';
@ -21,10 +21,7 @@ export default function SpacedriveInterface() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<QueryClientProvider client={queryClient} contextSharing={true}>
{/* The `context={defaultContext}` part is required for this to work on Windows. Why, idk, don't question it */}
{import.meta.env.DEV && (
<ReactQueryDevtools position="bottom-right" context={defaultContext} />
)}
<Devtools />
<MemoryRouter>
<AppRouterWrapper />
</MemoryRouter>
@ -33,6 +30,21 @@ export default function SpacedriveInterface() {
);
}
function Devtools() {
const debugState = useDebugState();
// The `context={defaultContext}` part is required for this to work on Windows. Why, idk, don't question it
return debugState.reactQueryDevtools !== 'disabled' ? (
<ReactQueryDevtools
position="bottom-right"
context={defaultContext}
toggleButtonProps={{
className: debugState.reactQueryDevtools === 'invisible' ? 'opacity-0' : ''
}}
/>
) : null;
}
// This can't go in `<SpacedriveInterface />` cause it needs the router context but it can't go in `<AppRouter />` because that requires this context
function AppRouterWrapper() {
const navigate = useNavigate();

View file

@ -1,12 +1,12 @@
import * as ToastPrimitive from '@radix-ui/react-toast';
import { useCurrentLibrary } from '@sd/client';
import clsx from 'clsx';
import { Suspense, useEffect, useState } from 'react';
import { IconoirProvider } from 'iconoir-react';
import { Suspense } from 'react';
import { Outlet } from 'react-router-dom';
import { Sidebar } from './components/layout/Sidebar';
import { Toasts } from './components/primitive/Toasts';
import { useOperatingSystem } from './hooks/useOperatingSystem';
import { useToasts } from './hooks/useToasts';
export function AppLayout() {
const { libraries } = useCurrentLibrary();
@ -19,111 +19,33 @@ export function AppLayout() {
return (
<div
className={clsx(
// App level styles
'flex h-screen overflow-hidden text-ink select-none cursor-default',
os === 'macOS' && 'rounded-[10px] has-blur-effects',
os !== 'browser' && os !== 'windows' && 'border border-app-frame'
)}
onContextMenu={(e) => {
// TODO: allow this on some UI text at least / disable default browser context menu
e.preventDefault();
return false;
}}
className={clsx(
'flex flex-row h-screen overflow-hidden text-gray-900 select-none dark:text-white cursor-default',
os === 'macOS' && 'rounded-xl',
os !== 'browser' && os !== 'windows' && 'border border-gray-200 dark:border-gray-500'
)}
>
<Sidebar />
<div className="relative flex w-full h-screen max-h-screen bg-white dark:bg-gray-650">
<Suspense>
<Outlet />
</Suspense>
<div className="relative flex w-full">
<IconoirProvider
iconProps={{
strokeWidth: 1.8,
width: '1em',
height: '1em'
}}
>
<Suspense fallback={<div className="w-screen h-screen bg-app" />}>
<Outlet />
</Suspense>
</IconoirProvider>
</div>
<Toasts />
</div>
);
}
function Toasts() {
const { toasts, addToast, removeToast } = useToasts();
// useEffect(() => {
// setTimeout(() => {
// addToast({
// title: 'Spacedrop',
// subtitle: 'Someone tried to send you a file. Accept it?',
// actionButton: {
// text: 'Accept',
// onClick: () => {
// console.log('Bruh');
// }
// }
// });
// }, 2000);
// }, []);
return (
<div className="fixed right-0 flex">
<ToastPrimitive.Provider>
<>
{toasts.map((toast) => (
<ToastPrimitive.Root
key={toast.id}
open={true}
onOpenChange={() => removeToast(toast)}
duration={toast.duration || 3000}
className={clsx(
'w-80 m-4 shadow-lg rounded-lg',
'bg-gray-800/20 backdrop-blur',
'radix-state-open:animate-toast-slide-in-bottom md:radix-state-open:animate-toast-slide-in-right',
'radix-state-closed:animate-toast-hide',
'radix-swipe-end:animate-toast-swipe-out',
'translate-x-radix-toast-swipe-move-x',
'radix-swipe-cancel:translate-x-0 radix-swipe-cancel:duration-200 radix-swipe-cancel:ease-[ease]',
'focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-opacity-75 border-white/10 border-2 shadow-2xl'
)}
>
<div className="flex">
<div className="flex items-center flex-1 w-0 py-4 pl-5">
<div className="w-full radix">
<ToastPrimitive.Title className="text-sm font-medium text-gray-900 dark:text-gray-100">
{toast.title}
</ToastPrimitive.Title>
{toast.subtitle && (
<ToastPrimitive.Description className="mt-1 text-sm text-gray-700 dark:text-gray-400">
{toast.subtitle}
</ToastPrimitive.Description>
)}
</div>
</div>
<div className="flex">
<div className="flex flex-col px-3 py-2 space-y-1">
<div className="flex flex-1 h-0">
{toast.actionButton && (
<ToastPrimitive.Action
altText="view now"
className="flex items-center justify-center w-full px-3 py-2 text-sm font-medium border border-transparent rounded-lg text-primary dark:text-primary hover:bg-white/10 focus:z-10 focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-opacity-75"
onClick={(e) => {
e.preventDefault();
toast.actionButton?.onClick();
removeToast(toast);
}}
>
{toast.actionButton.text || 'Open'}
</ToastPrimitive.Action>
)}
</div>
<div className="flex flex-1 h-0">
<ToastPrimitive.Close className="flex items-center justify-center w-full px-3 py-2 text-sm font-medium text-gray-700 border border-transparent rounded-lg dark:text-gray-100 hover:bg-white/10 focus:z-10 focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-opacity-75">
Dismiss
</ToastPrimitive.Close>
</div>
</div>
</div>
</div>
</ToastPrimitive.Root>
))}
<ToastPrimitive.Viewport />
</>
</ToastPrimitive.Provider>
</div>
);
}

View file

@ -1,44 +1,43 @@
import loadable from '@loadable/component';
import { lazy } from '@loadable/component';
import { useCurrentLibrary, useInvalidateQuery } from '@sd/client';
import { Suspense } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { AppLayout } from './AppLayout';
import { useKeybindHandler } from './hooks/useKeyboardHandler';
// Using React.lazy breaks hot reload so we don't use it.
const DebugScreen = loadable(() => import('./screens/Debug'));
const SettingsScreen = loadable(() => import('./screens/settings/Settings'));
const TagExplorer = loadable(() => import('./screens/TagExplorer'));
const PhotosScreen = loadable(() => import('./screens/Photos'));
const OverviewScreen = loadable(() => import('./screens/Overview'));
const ContentScreen = loadable(() => import('./screens/Content'));
const LocationExplorer = loadable(() => import('./screens/LocationExplorer'));
const OnboardingScreen = loadable(() => import('./components/onboarding/Onboarding'));
const NotFound = loadable(() => import('./NotFound'));
const DebugScreen = lazy(() => import('./screens/Debug'));
const SettingsScreen = lazy(() => import('./screens/settings/Settings'));
const TagExplorer = lazy(() => import('./screens/TagExplorer'));
const PhotosScreen = lazy(() => import('./screens/Photos'));
const OverviewScreen = lazy(() => import('./screens/Overview'));
const ContentScreen = lazy(() => import('./screens/Content'));
const LocationExplorer = lazy(() => import('./screens/LocationExplorer'));
const OnboardingScreen = lazy(() => import('./components/onboarding/Onboarding'));
const NotFound = lazy(() => import('./NotFound'));
const AppearanceSettings = loadable(() => import('./screens/settings/client/AppearanceSettings'));
const ExtensionSettings = loadable(() => import('./screens/settings/client/ExtensionsSettings'));
const GeneralSettings = loadable(() => import('./screens/settings/client/GeneralSettings'));
const KeybindingSettings = loadable(() => import('./screens/settings/client/KeybindingSettings'));
const PrivacySettings = loadable(() => import('./screens/settings/client/PrivacySettings'));
const AboutSpacedrive = loadable(() => import('./screens/settings/info/AboutSpacedrive'));
const Changelog = loadable(() => import('./screens/settings/info/Changelog'));
const Support = loadable(() => import('./screens/settings/info/Support'));
const ContactsSettings = loadable(() => import('./screens/settings/library/ContactsSettings'));
const KeysSettings = loadable(() => import('./screens/settings/library/KeysSetting'));
const LibraryGeneralSettings = loadable(
const AppearanceSettings = lazy(() => import('./screens/settings/client/AppearanceSettings'));
const ExtensionSettings = lazy(() => import('./screens/settings/client/ExtensionsSettings'));
const GeneralSettings = lazy(() => import('./screens/settings/client/GeneralSettings'));
const KeybindingSettings = lazy(() => import('./screens/settings/client/KeybindingSettings'));
const PrivacySettings = lazy(() => import('./screens/settings/client/PrivacySettings'));
const AboutSpacedrive = lazy(() => import('./screens/settings/info/AboutSpacedrive'));
const Changelog = lazy(() => import('./screens/settings/info/Changelog'));
const Support = lazy(() => import('./screens/settings/info/Support'));
const ContactsSettings = lazy(() => import('./screens/settings/library/ContactsSettings'));
const KeysSettings = lazy(() => import('./screens/settings/library/KeysSetting'));
const LibraryGeneralSettings = lazy(
() => import('./screens/settings/library/LibraryGeneralSettings')
);
const LocationSettings = loadable(() => import('./screens/settings/library/LocationSettings'));
const NodesSettings = loadable(() => import('./screens/settings/library/NodesSettings'));
const SecuritySettings = loadable(() => import('./screens/settings/library/SecuritySettings'));
const SharingSettings = loadable(() => import('./screens/settings/library/SharingSettings'));
const SyncSettings = loadable(() => import('./screens/settings/library/SyncSettings'));
const TagsSettings = loadable(() => import('./screens/settings/library/TagsSettings'));
const ExperimentalSettings = loadable(() => import('./screens/settings/node/ExperimentalSettings'));
const LibrarySettings = loadable(() => import('./screens/settings/node/LibrariesSettings'));
const P2PSettings = loadable(() => import('./screens/settings/node/P2PSettings'));
const LocationSettings = lazy(() => import('./screens/settings/library/LocationSettings'));
const NodesSettings = lazy(() => import('./screens/settings/library/NodesSettings'));
const SecuritySettings = lazy(() => import('./screens/settings/library/SecuritySettings'));
const SharingSettings = lazy(() => import('./screens/settings/library/SharingSettings'));
const SyncSettings = lazy(() => import('./screens/settings/library/SyncSettings'));
const TagsSettings = lazy(() => import('./screens/settings/library/TagsSettings'));
const ExperimentalSettings = lazy(() => import('./screens/settings/node/ExperimentalSettings'));
const LibrarySettings = lazy(() => import('./screens/settings/node/LibrariesSettings'));
const P2PSettings = lazy(() => import('./screens/settings/node/P2PSettings'));
export function AppRouter() {
const { library } = useCurrentLibrary();
@ -47,60 +46,56 @@ export function AppRouter() {
useInvalidateQuery();
return (
<Suspense>
<Routes>
<Route path="onboarding" element={<OnboardingScreen />} />
<Route element={<AppLayout />}>
{/* As we are caching the libraries in localStore so this *shouldn't* result is visual problems unless something else is wrong */}
{library === undefined ? (
<Route
path="*"
element={
<h1 className="p-4 text-white">
Please select or create a library in the sidebar.
</h1>
}
/>
) : (
<>
<Route index element={<Navigate to="/overview" />} />
<Route path="overview" element={<OverviewScreen />} />
<Route path="content" element={<ContentScreen />} />
<Route path="photos" element={<PhotosScreen />} />
<Route path="debug" element={<DebugScreen />} />
<Route path={'settings'} element={<SettingsScreen />}>
<Route index element={<GeneralSettings />} />
<Route path="general" element={<GeneralSettings />} />
<Route path="appearance" element={<AppearanceSettings />} />
<Route path="keybindings" element={<KeybindingSettings />} />
<Route path="extensions" element={<ExtensionSettings />} />
<Route path="p2p" element={<P2PSettings />} />
<Route path="contacts" element={<ContactsSettings />} />
<Route path="experimental" element={<ExperimentalSettings />} />
<Route path="keys" element={<KeysSettings />} />
<Route path="libraries" element={<LibrarySettings />} />
<Route path="security" element={<SecuritySettings />} />
<Route path="locations" element={<LocationSettings />} />
<Route path="sharing" element={<SharingSettings />} />
<Route path="sync" element={<SyncSettings />} />
<Route path="tags" element={<TagsSettings />} />
<Route path="library" element={<LibraryGeneralSettings />} />
<Route path="locations" element={<LocationSettings />} />
<Route path="tags" element={<TagsSettings />} />
<Route path="nodes" element={<NodesSettings />} />
<Route path="keys" element={<KeysSettings />} />
<Route path="privacy" element={<PrivacySettings />} />
<Route path="about" element={<AboutSpacedrive />} />
<Route path="changelog" element={<Changelog />} />
<Route path="support" element={<Support />} />
</Route>
<Route path="location/:id" element={<LocationExplorer />} />
<Route path="tag/:id" element={<TagExplorer />} />
<Route path="*" element={<NotFound />} />
</>
)}
</Route>
</Routes>
</Suspense>
<Routes>
<Route path="onboarding" element={<OnboardingScreen />} />
<Route element={<AppLayout />}>
{/* As we are caching the libraries in localStore so this *shouldn't* result is visual problems unless something else is wrong */}
{library === undefined ? (
<Route
path="*"
element={
<h1 className="p-4 text-white">Please select or create a library in the sidebar.</h1>
}
/>
) : (
<>
<Route index element={<Navigate to="/overview" />} />
<Route path="overview" element={<OverviewScreen />} />
<Route path="content" element={<ContentScreen />} />
<Route path="photos" element={<PhotosScreen />} />
<Route path="debug" element={<DebugScreen />} />
<Route path={'settings'} element={<SettingsScreen />}>
<Route index element={<GeneralSettings />} />
<Route path="general" element={<GeneralSettings />} />
<Route path="appearance" element={<AppearanceSettings />} />
<Route path="keybindings" element={<KeybindingSettings />} />
<Route path="extensions" element={<ExtensionSettings />} />
<Route path="p2p" element={<P2PSettings />} />
<Route path="contacts" element={<ContactsSettings />} />
<Route path="experimental" element={<ExperimentalSettings />} />
<Route path="keys" element={<KeysSettings />} />
<Route path="libraries" element={<LibrarySettings />} />
<Route path="security" element={<SecuritySettings />} />
<Route path="locations" element={<LocationSettings />} />
<Route path="sharing" element={<SharingSettings />} />
<Route path="sync" element={<SyncSettings />} />
<Route path="tags" element={<TagsSettings />} />
<Route path="library" element={<LibraryGeneralSettings />} />
<Route path="locations" element={<LocationSettings />} />
<Route path="tags" element={<TagsSettings />} />
<Route path="nodes" element={<NodesSettings />} />
<Route path="keys" element={<KeysSettings />} />
<Route path="privacy" element={<PrivacySettings />} />
<Route path="about" element={<AboutSpacedrive />} />
<Route path="changelog" element={<Changelog />} />
<Route path="support" element={<Support />} />
</Route>
<Route path="location/:id" element={<LocationExplorer />} />
<Route path="tag/:id" element={<TagExplorer />} />
<Route path="*" element={<NotFound />} />
</>
)}
</Route>
</Routes>
);
}

View file

@ -12,13 +12,13 @@ export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
<div
data-tauri-drag-region
role="alert"
className="flex flex-col items-center justify-center w-screen h-screen p-4 border border-gray-200 rounded-lg dark:border-gray-650 bg-gray-50 dark:bg-gray-650 dark:text-white"
className="flex flex-col items-center justify-center w-screen h-screen p-4 border rounded-lg border-app-divider bg-app"
>
<p className="m-3 text-sm font-bold text-gray-400">APP CRASHED</p>
<h1 className="text-2xl font-bold">We're past the event horizon...</h1>
<pre className="m-2">Error: {error.message}</pre>
<div className="flex flex-row space-x-2">
<Button variant="primary" className="mt-2" onClick={resetErrorBoundary}>
<p className="m-3 text-sm font-bold text-ink-faint">APP CRASHED</p>
<h1 className="text-2xl font-bold text-ink">We're past the event horizon...</h1>
<pre className="m-2 text-ink">Error: {error.message}</pre>
<div className="flex flex-row space-x-2 text-ink">
<Button variant="accent" className="mt-2" onClick={resetErrorBoundary}>
Reload
</Button>
<Button

View file

@ -7,12 +7,12 @@ export default function NotFound() {
<div
data-tauri-drag-region
role="alert"
className="flex flex-col items-center justify-center w-full h-full p-4 rounded-lg dark:text-white"
className="flex flex-col items-center justify-center w-full h-full p-4 rounded-lg"
>
<p className="m-3 text-sm font-semibold text-gray-500 uppercase">Error: 404</p>
<p className="m-3 text-sm font-semibold uppercase text-ink-faint">Error: 404</p>
<h1 className="text-4xl font-bold">You chose nothingness.</h1>
<div className="flex flex-row space-x-2">
<Button variant="primary" className="mt-4" onClick={() => navigate(-1)}>
<Button variant="accent" className="mt-4" onClick={() => navigate(-1)}>
Go Back
</Button>
</div>

View file

@ -26,7 +26,7 @@ export function Device(props: DeviceProps) {
}
return (
<div className="w-full border border-gray-100 rounded-md bg-gray-50 dark:bg-gray-600 dark:border-gray-550">
<div className="w-full border rounded-md border-app-divider bg-app">
<div className="flex flex-row items-center px-4 pt-2 pb-2">
<DotsSixVertical weight="bold" className="mr-3 opacity-30" />
{props.type === 'phone' && <DeviceMobileCamera weight="fill" size={20} className="mr-2" />}
@ -35,22 +35,18 @@ export function Device(props: DeviceProps) {
{props.type === 'server' && <Cloud weight="fill" size={20} className="mr-2" />}
<h3 className="font-semibold text-md">{props.name || 'Unnamed Device'}</h3>
<div className="flex flex-row space-x-1.5 mt-0.5">
<span className="font-semibold flex flex-row h-[19px] -mt-0.5 ml-3 py-0.5 px-1.5 text-[10px] rounded bg-gray-250 text-gray-500 dark:bg-gray-500 dark:text-gray-400">
<span className="font-semibold flex flex-row h-[19px] -mt-0.5 ml-3 py-0.5 px-1.5 text-[10px] rounded text-type-faint">
<LockClosedIcon className="w-3 h-3 mr-1 -ml-0.5 m-[1px]" />
P2P
</span>
</div>
<span className="font-semibold py-0.5 px-1.5 text-sm ml-2 text-gray-400 ">
{props.size}
</span>
<span className="font-semibold py-0.5 px-1.5 text-sm ml-2 ">{props.size}</span>
<div className="flex flex-grow" />
{props.runningJob && (
<div className="flex flex-row ml-5 bg-gray-300 bg-opacity-50 rounded-md dark:bg-gray-550">
<div className="flex flex-row ml-5 bg-opacity-50 rounded-md ">
<Loader />
<div className="flex flex-col p-2">
<span className="mb-[2px] -mt-1 truncate text-gray-450 text-tiny">
{props.runningJob.task}...
</span>
<span className="mb-[2px] -mt-1 truncate text-tiny">{props.runningJob.task}...</span>
<ProgressBar value={props.runningJob?.amount} total={100} />
</div>
</div>
@ -70,7 +66,7 @@ export function Device(props: DeviceProps) {
</div>
<div className="px-4 pb-3 mt-3">
{props.locations.length === 0 && (
<div className="w-full my-5 text-center text-gray-450">No locations</div>
<div className="w-full my-5 text-center">No locations</div>
)}
</div>
</div>

View file

@ -6,9 +6,10 @@ import { PropsWithChildren, useState } from 'react';
export default function CreateLibraryDialog({
children,
onSubmit
}: PropsWithChildren<{ onSubmit?: () => void }>) {
const [openCreateModal, setOpenCreateModal] = useState(false);
onSubmit,
open,
setOpen
}: PropsWithChildren<{ onSubmit?: () => void; open: boolean; setOpen: (state: boolean) => void }>) {
const [newLibName, setNewLibName] = useState('');
const queryClient = useQueryClient();
@ -16,7 +17,7 @@ export default function CreateLibraryDialog({
'library.create',
{
onSuccess: (library: any) => {
setOpenCreateModal(false);
setOpen(false);
queryClient.setQueryData(['library.list'], (libraries: any) => [
...(libraries || []),
@ -33,8 +34,8 @@ export default function CreateLibraryDialog({
return (
<Dialog
open={openCreateModal}
onOpenChange={setOpenCreateModal}
open={open}
setOpen={setOpen}
title="Create New Library"
description="Choose a name for your new library, you can configure this and more settings from the library settings later on."
ctaAction={() => createLibrary(newLibName)}
@ -48,6 +49,7 @@ export default function CreateLibraryDialog({
value={newLibName}
placeholder="My Cool Library"
onChange={(e) => setNewLibName(e.target.value)}
required
/>
</Dialog>
);

View file

@ -22,7 +22,7 @@ export default function DeleteLibraryDialog(
return (
<Dialog
open={openDeleteModal}
onOpenChange={setOpenDeleteModal}
setOpen={setOpenDeleteModal}
title="Delete Library"
description="Deleting a library will permanently the database, the files themselves will not be deleted."
ctaAction={() => {

View file

@ -1,4 +1,5 @@
import { ExplorerData, rspc, useCurrentLibrary, useExplorerStore } from '@sd/client';
import { useEffect, useState } from 'react';
import { Inspector } from '../explorer/Inspector';
import ExplorerContextMenu from './ExplorerContextMenu';
@ -13,6 +14,18 @@ export default function Explorer(props: Props) {
const expStore = useExplorerStore();
const { library } = useCurrentLibrary();
const [scrollSegments, setScrollSegments] = useState<{ [key: string]: number }>({});
const [separateTopBar, setSeparateTopBar] = useState<boolean>(false);
useEffect(() => {
setSeparateTopBar((oldValue) => {
const newValue = Object.values(scrollSegments).some((val) => val >= 5);
if (newValue !== oldValue) return newValue;
return oldValue;
});
}, [scrollSegments]);
rspc.useSubscription(['jobs.newThumbnail', { library_id: library!.uuid, arg: null }], {
onData: (cas_id) => {
expStore.addNewThumbnail(cas_id);
@ -22,16 +35,37 @@ export default function Explorer(props: Props) {
return (
<div className="relative">
<ExplorerContextMenu>
<div className="relative flex flex-col w-full bg-gray-650">
<TopBar />
<div className="relative flex flex-col w-full">
<TopBar showSeparator={separateTopBar} />
<div className="relative flex flex-row w-full max-h-full ">
<div className="relative flex flex-row w-full max-h-full app-background ">
{props.data && (
<VirtualizedList data={props.data.items || []} context={props.data.context} />
<VirtualizedList
data={props.data.items || []}
context={props.data.context}
onScroll={(y) => {
setScrollSegments((old) => {
return {
...old,
mainList: y
};
});
}}
/>
)}
{expStore.showInspector && (
<div className="flex min-w-[260px] max-w-[260px]">
<Inspector
onScroll={(e) => {
const y = (e.target as HTMLElement).scrollTop;
setScrollSegments((old) => {
return {
...old,
inspector: y
};
});
}}
key={props.data?.items[expStore.selectedRowIndex]?.id}
data={props.data?.items[expStore.selectedRowIndex]}
/>

View file

@ -1,14 +1,7 @@
import {
getExplorerStore,
useExplorerStore,
useLibraryMutation,
useLibraryQuery
} from '@sd/client';
import { getExplorerStore, useLibraryMutation, useLibraryQuery, usePlatform } from '@sd/client';
import { ContextMenu as CM } from '@sd/ui';
import {
ArrowBendUpRight,
FilePlus,
FileX,
LockSimple,
Package,
Plus,
@ -17,23 +10,24 @@ import {
Trash,
TrashSimple
} from 'phosphor-react';
import { PropsWithChildren } from 'react';
import { useSnapshot } from 'valtio';
import { PropsWithChildren, useMemo } from 'react';
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
const AssignTagMenuItems = (props: { objectId: number }) => {
const tags = useLibraryQuery(['tags.list'], { suspense: true });
const tagsForObject = useLibraryQuery(['tags.getForObject', props.objectId], { suspense: true });
const { mutate: assignTag } = useLibraryMutation('tags.assign');
return (
<>
{tags.data?.map((tag) => {
{tags.data?.map((tag, index) => {
const active = !!tagsForObject.data?.find((t) => t.id === tag.id);
return (
<CM.Item
key={tag.id}
keybind={`${index + 1}`}
onClick={(e) => {
e.preventDefault();
if (props.objectId === null) return;
@ -62,22 +56,44 @@ const AssignTagMenuItems = (props: { objectId: number }) => {
export default function ExplorerContextMenu(props: PropsWithChildren) {
const store = getExplorerStore();
// const { mutate: generateThumbsForLocation } = useLibraryMutation(
// 'jobs.generateThumbsForLocation'
// );
const platform = usePlatform();
const os = useOperatingSystem();
const osFileBrowserName = useMemo(() => {
if (os === 'macOS') {
return 'Finder';
} else {
return 'Explorer';
}
}, [os]);
return (
<div className="relative">
<CM.ContextMenu trigger={props.children}>
<CM.Item label="Open" />
<CM.Item label="Open" keybind="⌘O" />
<CM.Item label="Open with..." />
<CM.Separator />
<CM.Item label="Quick view" />
<CM.Item label="Open in Finder" />
<CM.Item label="Quick view" keybind="␣" />
{platform.openPath && (
<CM.Item
label={`Open in ${osFileBrowserName}`}
keybind="⌘Y"
onClick={() => {
console.log('TODO', store.contextMenuActiveObject);
platform.openPath!('/Users/oscar/Desktop'); // TODO: Work out the file path from the backend
}}
/>
)}
<CM.Separator />
<CM.Item label="Rename" />
<CM.Item label="Duplicate" />
<CM.Item label="Duplicate" keybind="⌘D" />
<CM.Separator />
@ -103,18 +119,20 @@ export default function ExplorerContextMenu(props: PropsWithChildren) {
</CM.SubMenu>
)}
<CM.SubMenu label="More actions..." icon={Plus}>
<CM.Item label="Encrypt" icon={LockSimple} />
<CM.Item label="Compress" icon={Package} />
<CM.Item label="Encrypt" icon={LockSimple} keybind="⌘E" />
<CM.Item label="Compress" icon={Package} keybind="⌘B" />
<CM.SubMenu label="Convert to" icon={ArrowBendUpRight}>
<CM.Item label="PNG" />
<CM.Item label="WebP" />
</CM.SubMenu>
<CM.Item label="Rescan Directory" icon={Package} />
<CM.Item label="Regen Thumbnails" icon={Package} />
<CM.Item variant="danger" label="Secure delete" icon={TrashSimple} />
</CM.SubMenu>
<CM.Separator />
<CM.Item icon={Trash} label="Delete" variant="danger" />
<CM.Item icon={Trash} label="Delete" variant="danger" keybind="⌘DEL" />
</CM.ContextMenu>
</div>
);

View file

@ -4,11 +4,11 @@ import { PropsWithChildren, useState } from 'react';
import Slider from '../primitive/Slider';
function Heading({ children }: PropsWithChildren) {
return <div className="text-xs font-semibold text-gray-300">{children}</div>;
return <div className="text-xs font-semibold text-ink-dull">{children}</div>;
}
function SubHeading({ children }: PropsWithChildren) {
return <div className="mb-1 text-xs font-medium text-gray-300">{children}</div>;
return <div className="mb-1 text-xs font-medium text-ink-dull">{children}</div>;
}
export function ExplorerOptionsPanel() {

View file

@ -1,19 +1,11 @@
import { ChevronLeftIcon, ChevronRightIcon, TagIcon } from '@heroicons/react/24/outline';
import {
OperatingSystem,
getExplorerStore,
useExplorerStore,
useLibraryMutation
} from '@sd/client';
import { Dropdown, OverlayPanel } from '@sd/ui';
import { TagIcon as TagIconSolid } from '@heroicons/react/24/solid';
import { getExplorerStore, useExplorerStore, useLibraryMutation } from '@sd/client';
import { Button, Input, OverlayPanel, cva, tw } from '@sd/ui';
import clsx from 'clsx';
import {
Aperture,
ArrowsClockwise,
Cloud,
FilmStrip,
IconProps,
Image,
Key,
List,
MonitorPlay,
@ -21,7 +13,7 @@ import {
SidebarSimple,
SquaresFour
} from 'phosphor-react';
import { DetailedHTMLProps, HTMLAttributes, forwardRef, useEffect, useRef } from 'react';
import { forwardRef, useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
@ -34,38 +26,47 @@ import { Tooltip } from '../tooltip/Tooltip';
import { ExplorerOptionsPanel } from './ExplorerOptionsPanel';
export interface TopBarButtonProps {
icon: React.ComponentType<IconProps>;
group?: boolean;
children: React.ReactNode;
rounding?: 'none' | 'left' | 'right' | 'both';
active?: boolean;
left?: boolean;
right?: boolean;
className?: string;
onClick?: () => void;
}
const TopBarButton = forwardRef<HTMLButtonElement, TopBarButtonProps>(
({ icon: Icon, left, right, group, active, className, ...props }, ref) => {
return (
<button
{...props}
ref={ref}
className={clsx(
'mr-[1px] flex py-0.5 px-0.5 text-md font-medium hover:bg-gray-150 dark:transparent dark:hover:bg-gray-550 rounded-md open:dark:bg-gray-550 transition-colors duration-100 outline-none !cursor-normal',
{
'rounded-r-none rounded-l-none': group && !left && !right,
'rounded-r-none': group && left,
'rounded-l-none': group && right,
'dark:bg-gray-500': active
},
className
)}
>
<Icon weight={'regular'} className="m-0.5 w-5 h-5 text-gray-450 dark:text-gray-150" />
</button>
);
// export const TopBarIcon = (icon: any) => tw(icon)`m-0.5 w-5 h-5 text-ink-dull`;
const topBarButtonStyle = cva(
'border-none text-ink hover:text-ink mr-[1px] flex py-0.5 px-0.5 text-md font-medium transition-colors duration-100 outline-none hover:bg-app-selected radix-state-open:bg-app-selected',
{
variants: {
active: {
true: 'bg-app-selected',
false: 'bg-transparent'
},
rounding: {
none: 'rounded-none',
left: 'rounded-l-md rounded-r-none',
right: 'rounded-r-md rounded-l-none',
both: 'rounded-md'
}
},
defaultVariants: {
active: false,
rounding: 'both'
}
}
);
const TOP_BAR_ICON_STYLE = 'm-0.5 w-5 h-5 text-ink-dull';
const TopBarButton = forwardRef<HTMLButtonElement, TopBarButtonProps>((props, ref) => {
return (
<Button {...props} ref={ref} className={clsx(topBarButtonStyle(props), props.className)}>
{props.children}
</Button>
);
});
const SearchBar = forwardRef<HTMLInputElement, DefaultProps>((props, forwardedRef) => {
const {
register,
@ -86,7 +87,7 @@ const SearchBar = forwardRef<HTMLInputElement, DefaultProps>((props, forwardedRe
return (
<form onSubmit={handleSubmit(() => null)} className="relative flex h-7">
<input
<Input
ref={(el) => {
ref(el);
@ -94,62 +95,38 @@ const SearchBar = forwardRef<HTMLInputElement, DefaultProps>((props, forwardedRe
else if (forwardedRef) forwardedRef.current = el;
}}
placeholder="Search"
className="peer w-32 h-[30px] focus:w-52 text-sm p-3 rounded-lg outline-none focus:ring-2 placeholder-gray-400 dark:placeholder-gray-450 bg-[#F6F2F6] border border-gray-50 shadow-md dark:bg-gray-600 dark:border-gray-550 focus:ring-gray-100 dark:focus:ring-gray-550 dark:focus:bg-gray-800 transition-all"
className="w-32 transition-all focus:w-52"
{...searchField}
/>
<div
className={clsx(
'space-x-1 absolute top-[1px] right-1 peer-focus:invisible pointer-events-none',
isDirty && 'hidden'
)}
>
<div className={clsx('space-x-1 absolute right-1 peer-focus:invisible pointer-events-none')}>
{platform === 'browser' ? (
<Shortcut chars="/" aria-label={'Press slash to focus search bar'} />
<Shortcut chars="⌘F" aria-label={'Press Command-F to focus search bar'} />
) : os === 'macOS' ? (
<Shortcut chars="⌘F" aria-label={'Press Command-F to focus search bar'} />
) : (
<Shortcut chars="CTRL+F" aria-label={'Press CTRL-F to focus search bar'} />
)}
{/* <Shortcut chars="S" /> */}
</div>
</form>
);
});
export type TopBarProps = DefaultProps;
export type TopBarProps = DefaultProps & {
showSeparator?: boolean;
};
export const TopBar: React.FC<TopBarProps> = (props) => {
const platform = useOperatingSystem(false);
const os = useOperatingSystem(true);
const store = useExplorerStore();
const { mutate: generateThumbsForLocation } = useLibraryMutation(
'jobs.generateThumbsForLocation',
{
onMutate: (data) => {
// console.log('GenerateThumbsForLocation', data);
}
}
);
const { mutate: identifyUniqueFiles } = useLibraryMutation('jobs.identifyUniqueFiles', {
onMutate: (data) => {
// console.log('IdentifyUniqueFiles', data);
},
onError: (error) => {
console.error('IdentifyUniqueFiles', error);
}
});
const { mutate: objectValidator } = useLibraryMutation(
'jobs.objectValidator',
{
onMutate: (data) => {
// console.log('ObjectValidator', data);
}
}
);
// const { mutate: generateThumbsForLocation } = useLibraryMutation(
// 'jobs.generateThumbsForLocation'
// );
// const { mutate: identifyUniqueFiles } = useLibraryMutation('jobs.identifyUniqueFiles');
// const { mutate: objectValidator } = useLibraryMutation('jobs.objectValidator');
const navigate = useNavigate();
@ -214,14 +191,21 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
<>
<div
data-tauri-drag-region
className="flex h-[2.95rem] -mt-0.5 max-w z-10 pl-3 flex-shrink-0 items-center dark:bg-gray-700 border-gray-100 !bg-opacity-80 backdrop-blur overflow-hidden rounded-tl-md"
className={clsx(
'flex h-[2.95rem] -mt-0.5 max-w z-10 pl-3 flex-shrink-0 items-center border-transparent border-b bg-app overflow-hidden transition-[background-color] transition-[border-color] duration-250 ease-out',
props.showSeparator && 'top-bar-blur !bg-app/90'
)}
>
<div className="flex ">
<div className="flex">
<Tooltip label="Navigate back">
<TopBarButton icon={ChevronLeftIcon} onClick={() => navigate(-1)} />
<TopBarButton onClick={() => navigate(-1)}>
<ChevronLeftIcon className={TOP_BAR_ICON_STYLE} />
</TopBarButton>
</Tooltip>
<Tooltip label="Navigate forward">
<TopBarButton icon={ChevronRightIcon} onClick={() => navigate(1)} />
<TopBarButton onClick={() => navigate(1)}>
<ChevronRightIcon className={TOP_BAR_ICON_STYLE} />
</TopBarButton>
</Tooltip>
</div>
@ -235,30 +219,31 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
<div className="flex mx-8">
<Tooltip label="Grid view">
<TopBarButton
group
left
rounding="left"
active={store.layoutMode === 'grid'}
icon={SquaresFour}
onClick={() => (getExplorerStore().layoutMode = 'grid')}
/>
>
<SquaresFour className={TOP_BAR_ICON_STYLE} />
</TopBarButton>
</Tooltip>
<Tooltip label="List view">
<TopBarButton
group
rounding="none"
active={store.layoutMode === 'list'}
icon={Rows}
onClick={() => (getExplorerStore().layoutMode = 'list')}
/>
>
<Rows className={TOP_BAR_ICON_STYLE} />
</TopBarButton>
</Tooltip>
<Tooltip label="Media view">
<TopBarButton
group
right
rounding="right"
active={store.layoutMode === 'media'}
icon={MonitorPlay}
onClick={() => (getExplorerStore().layoutMode = 'media')}
/>
>
<MonitorPlay className={TOP_BAR_ICON_STYLE} />
</TopBarButton>
</Tooltip>
</div>
@ -269,19 +254,32 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
className="focus:outline-none"
trigger={
// <Tooltip label="Major Key Alert">
<TopBarButton icon={Key} />
<TopBarButton>
<Key className={TOP_BAR_ICON_STYLE} />
</TopBarButton>
// </Tooltip>
}
>
<div className="block w-[350px]">
<KeyManager />
<KeyManager className={TOP_BAR_ICON_STYLE} />
</div>
</OverlayPanel>
<Tooltip label="Tag Assign Mode">
<TopBarButton icon={TagIcon} />
<TopBarButton
onClick={() => (getExplorerStore().tagAssignMode = !store.tagAssignMode)}
active={store.tagAssignMode}
>
{store.tagAssignMode ? (
<TagIconSolid className={TOP_BAR_ICON_STYLE} />
) : (
<TagIcon className={TOP_BAR_ICON_STYLE} />
)}
</TopBarButton>
</Tooltip>
<Tooltip label="Refresh">
<TopBarButton icon={ArrowsClockwise} />
<TopBarButton>
<ArrowsClockwise className={TOP_BAR_ICON_STYLE} />
</TopBarButton>
</Tooltip>
</div>
</div>
@ -290,7 +288,9 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
className="focus:outline-none"
trigger={
// <Tooltip label="Major Key Alert">
<TopBarButton icon={List} className="my-2" />
<TopBarButton className="my-2">
<List className={TOP_BAR_ICON_STYLE} />
</TopBarButton>
// </Tooltip>
}
>
@ -302,8 +302,13 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
active={store.showInspector}
onClick={() => (getExplorerStore().showInspector = !store.showInspector)}
className="my-2"
icon={SidebarSimple}
/>
>
{store.showInspector ? (
<SidebarSimple className={TOP_BAR_ICON_STYLE} />
) : (
<SidebarSimple className={TOP_BAR_ICON_STYLE} />
)}
</TopBarButton>
{/* <Dropdown
// className="absolute block h-6 w-44 top-2 right-4"
align="right"

View file

@ -1,10 +1,24 @@
import { ExplorerItem, getExplorerStore } from '@sd/client';
import { cva, tw } from '@sd/ui';
import clsx from 'clsx';
import { HTMLAttributes } from 'react';
import FileThumb from './FileThumb';
import { isObject } from './utils';
const NameArea = tw.div`flex justify-center`;
const nameContainerStyles = cva(
'px-1.5 py-[1px] truncate text-center rounded-md text-xs font-medium cursor-default',
{
variants: {
selected: {
true: 'bg-accent text-white'
}
}
}
);
interface Props extends HTMLAttributes<HTMLDivElement> {
data: ExplorerItem;
selected: boolean;
@ -12,12 +26,6 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
}
function FileItem({ data, selected, index, ...rest }: Props) {
// const store = useExplorerStore();
// store.layoutMode;
// props.index === store.selectedRowIndex
const isVid = isVideo(data.extension || '');
return (
@ -28,6 +36,7 @@ function FileItem({ data, selected, index, ...rest }: Props) {
getExplorerStore().contextMenuObjectId = objectId;
if (index != undefined) {
getExplorerStore().selectedRowIndex = index;
getExplorerStore().contextMenuActiveObject = isObject(data) ? data : data.object;
}
}
}}
@ -40,7 +49,7 @@ function FileItem({ data, selected, index, ...rest }: Props) {
className={clsx(
'border-2 border-transparent rounded-lg text-center mb-1 active:translate-y-[1px]',
{
'bg-gray-50 dark:bg-gray-750': selected
'bg-app-selected/30': selected
}
)}
>
@ -51,33 +60,26 @@ function FileItem({ data, selected, index, ...rest }: Props) {
>
<FileThumb
className={clsx(
'border-4 border-gray-250 shadow-md shadow-gray-750 object-cover max-w-full max-h-full w-auto overflow-hidden',
isVid && 'border-gray-950 rounded border-x-0 border-y-[9px]'
'border-4 border-white shadow shadow-black/40 object-cover max-w-full max-h-full w-auto overflow-hidden',
isVid && '!border-black rounded border-x-0 border-y-[9px]'
)}
data={data}
kind={data.extension === 'zip' ? 'zip' : isVid ? 'video' : 'other'}
size={getExplorerStore().gridItemSize}
/>
{data?.extension && isVid && (
<div className="absolute bottom-4 font-semibold opacity-70 right-2 py-0.5 px-1 text-[9px] uppercase bg-gray-800 rounded">
<div className="absolute bottom-4 font-semibold opacity-70 right-2 py-0.5 px-1 text-[9px] uppercase bg-black/60 rounded">
{data.extension}
</div>
)}
</div>
</div>
<div className="flex justify-center">
<span
className={clsx(
'px-1.5 py-[1px] truncate text-center rounded-md text-xs font-medium text-gray-550 dark:text-gray-300 cursor-default ',
{
'bg-primary !text-white': selected
}
)}
>
<NameArea>
<span className={nameContainerStyles({ selected })}>
{data?.name}
{data?.extension && `.${data.extension}`}
</span>
</div>
</NameArea>
</div>
);
}

View file

@ -16,7 +16,7 @@ function FileRow({ data, index, selected, ...props }: Props) {
{...props}
className={clsx(
'table-body-row mr-2 flex w-full flex-row rounded-lg border-2',
selected ? 'border-primary-500' : 'border-transparent',
selected ? 'border-accent' : 'border-transparent',
index % 2 == 0 && 'bg-[#00000006] dark:bg-[#00000030]'
)}
>

View file

@ -1,12 +1,10 @@
import videoSvg from '@sd/assets/svgs/video.svg';
import zipSvg from '@sd/assets/svgs/zip.svg';
import { getExplorerStore, usePlatform } from '@sd/client';
import { usePlatform } from '@sd/client';
import { useExplorerStore } from '@sd/client';
import { ExplorerItem } from '@sd/client';
import clsx from 'clsx';
import { useState } from 'react';
import { Suspense, lazy, useMemo } from 'react';
import { useSnapshot } from 'valtio';
import { Folder } from '../icons/Folder';
import { isObject, isPath } from './utils';
@ -78,11 +76,11 @@ export default function FileThumb({ data, ...props }: Props) {
>
<svg
// BACKGROUND
className="absolute -translate-x-1/2 -translate-y-1/2 pointer-events-none top-1/2 left-1/2 fill-gray-150 dark:fill-gray-550"
className="absolute -translate-x-1/2 -translate-y-1/2 pointer-events-none top-1/2 left-1/2 fill-app-box"
width="100%"
height="100%"
viewBox="0 0 65 81"
style={{ filter: 'drop-shadow(0px 5px 2px rgb(0 0 0 / 0.05))' }}
style={{ filter: 'drop-shadow(0px 2px 1px rgb(0 0 0 / 0.15))' }}
>
<path d="M0 8C0 3.58172 3.58172 0 8 0H39.6863C41.808 0 43.8429 0.842855 45.3431 2.34315L53.5 10.5L62.6569 19.6569C64.1571 21.1571 65 23.192 65 25.3137V73C65 77.4183 61.4183 81 57 81H8C3.58172 81 0 77.4183 0 73V8Z" />
</svg>
@ -103,8 +101,9 @@ export default function FileThumb({ data, ...props }: Props) {
// PEEL
width="28%"
height="28%"
className="absolute top-0 right-0 -translate-x-[35%] z-0 pointer-events-none fill-gray-50 dark:fill-gray-500"
viewBox="0 0 41 41"
className="absolute top-0 right-0 -translate-x-[40%] z-0 pointer-events-none fill-app-selected"
viewBox="0 0 40 40"
style={{ filter: 'drop-shadow(-3px 1px 1px rgb(0 0 0 / 0.05))' }}
>
<path d="M41.4116 40.5577H11.234C5.02962 40.5577 0 35.5281 0 29.3238V0L41.4116 40.5577Z" />
</svg>

View file

@ -9,6 +9,7 @@ import dayjs from 'dayjs';
import { Link } from 'phosphor-react';
import { useEffect, useState } from 'react';
import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip';
import FileThumb from './FileThumb';
import { Divider } from './inspector/Divider';
@ -17,12 +18,14 @@ import { MetaItem } from './inspector/MetaItem';
import Note from './inspector/Note';
import { isObject } from './utils';
interface Props {
interface Props extends DefaultProps<HTMLDivElement> {
context?: ExplorerContext;
data?: ExplorerItem;
}
export const Inspector = (props: Props) => {
const { context, data, ...elementProps } = props;
const { data: types } = useQuery(
['_file-types'],
() => import('../../constants/file-types.json')
@ -49,10 +52,13 @@ export const Inspector = (props: Props) => {
const isVid = isVideo(props.data?.extension || '');
return (
<div className="-mt-[50px] pt-[55px] pl-1.5 pr-1 w-full h-screen overflow-x-hidden custom-scroll inspector-scroll pb-[55px]">
<div
{...elementProps}
className="-mt-[50px] pt-[55px] pl-1.5 pr-1 w-full h-screen overflow-x-hidden custom-scroll inspector-scroll pb-[55px]"
>
{!!props.data && (
<>
<div className="flex bg-black items-center justify-center w-full h-64 mb-[10px] overflow-hidden rounded-lg ">
<div className="flex bg-sidebar items-center justify-center w-full h-64 mb-[10px] overflow-hidden rounded-lg ">
<FileThumb
iconClassNames="mx-10"
size={230}
@ -61,7 +67,7 @@ export const Inspector = (props: Props) => {
data={props.data}
/>
</div>
<div className="flex flex-col w-full pt-0.5 pb-1 overflow-hidden bg-white rounded-lg shadow select-text dark:shadow-gray-800/40 dark:bg-gray-550 dark:bg-opacity-40 border border-gray-550/70">
<div className="flex flex-col w-full pt-0.5 pb-1 overflow-hidden bg-app-box rounded-lg select-text shadow-app-shade/10 border border-app-line">
<h3 className="pt-2 pb-1 pl-3 text-base font-bold">
{props.data?.name}
{props.data?.extension && `.${props.data.extension}`}
@ -72,12 +78,12 @@ export const Inspector = (props: Props) => {
<FavoriteButton data={objectData} />
</Tooltip>
<Tooltip label="Share">
<Button size="sm" padding="sm">
<Button size="icon">
<ShareIcon className="w-[18px] h-[18px]" />
</Button>
</Tooltip>
<Tooltip label="Link">
<Button size="sm" padding="sm">
<Button size="icon">
<Link className="w-[18px] h-[18px]" />
</Button>
</Tooltip>

View file

@ -1,7 +1,7 @@
import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '@sd/client';
import { ExplorerContext, ExplorerItem } from '@sd/client';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { UIEventHandler, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useKey, useOnWindowResize } from 'rooks';
@ -9,15 +9,16 @@ import FileItem from './FileItem';
import FileRow from './FileRow';
import { isPath } from './utils';
const TOP_BAR_HEIGHT = 50;
const TOP_BAR_HEIGHT = 46;
const GRID_TEXT_AREA_HEIGHT = 25;
interface Props {
context: ExplorerContext;
data: ExplorerItem[];
onScroll?: (posY: number) => void;
}
export const VirtualizedList: React.FC<Props> = ({ data, context }) => {
export const VirtualizedList: React.FC<Props> = ({ data, context, onScroll }) => {
const scrollRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
@ -45,6 +46,19 @@ export const VirtualizedList: React.FC<Props> = ({ data, context }) => {
? explorerStore.gridItemSize + GRID_TEXT_AREA_HEIGHT
: explorerStore.listItemSize;
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
const onElementScroll = (event: Event) => {
onScroll?.((event.target as HTMLElement).scrollTop);
};
el.addEventListener('scroll', onElementScroll);
return () => el.removeEventListener('scroll', onElementScroll);
}, [scrollRef, onScroll]);
const rowVirtualizer = useVirtualizer({
count: amountOfRows,
getScrollElement: () => scrollRef.current,
@ -135,7 +149,7 @@ export const VirtualizedList: React.FC<Props> = ({ data, context }) => {
const item = data[index];
const isSelected = explorerStore.selectedRowIndex === index;
return (
<div key={index} className="w-32 h-32">
<div key={index} className="">
<div className="flex">
{item && (
<WrappedItem

View file

@ -1 +1 @@
export const Divider = () => <div className="w-full my-1 h-[1px] bg-gray-100 dark:bg-gray-550" />;
export const Divider = () => <div className="w-full my-1 h-[1px] bg-app-line/60" />;

View file

@ -30,7 +30,7 @@ export default function FavoriteButton(props: Props) {
};
return (
<Button onClick={toggleFavorite} size="sm" padding="sm">
<Button onClick={toggleFavorite} size="sm">
<Heart weight={favorite ? 'fill' : 'regular'} className="w-[18px] h-[18px]" />
</Button>
);

View file

@ -8,7 +8,7 @@ export const MetaItem = (props: MetaItemProps) => {
<div data-tip={props.value} className="flex flex-col px-4 py-1.5 meta-item">
{!!props.title && <h5 className="text-xs font-bold">{props.title}</h5>}
{typeof props.value === 'string' ? (
<p className="text-xs text-gray-600 break-all truncate dark:text-gray-300">{props.value}</p>
<p className="text-xs break-all truncate">{props.value}</p>
) : (
props.value
)}

View file

@ -1,11 +1,21 @@
import { EyeIcon, FolderIcon, PhotoIcon, XMarkIcon } from '@heroicons/react/24/outline';
import {
EllipsisHorizontalIcon,
EllipsisVerticalIcon,
EyeIcon,
FingerPrintIcon,
FolderIcon,
PhotoIcon,
XMarkIcon
} from '@heroicons/react/24/solid';
import { QuestionMarkCircleIcon } from '@heroicons/react/24/solid';
import { useLibraryQuery } from '@sd/client';
import { JobReport } from '@sd/client';
import { Button } from '@sd/ui';
import { Button, CategoryHeading, tw } from '@sd/ui';
import clsx from 'clsx';
import dayjs from 'dayjs';
import { ArrowsClockwise } from 'phosphor-react';
import { ArrowsClockwise, Pause } from 'phosphor-react';
import ProgressBar from '../primitive/ProgressBar';
import { Tooltip } from '../tooltip/Tooltip';
interface JobNiceData {
@ -13,20 +23,26 @@ interface JobNiceData {
icon: React.FC<React.ComponentProps<'svg'>>;
}
const NiceData: Record<string, JobNiceData> = {
const getNiceData = (job: JobReport): Record<string, JobNiceData> => ({
indexer: {
name: 'Indexed location',
name: `Indexed ${numberWithCommas(job.metadata?.data?.total_paths || 0)} paths at "${
job.metadata?.data?.location_path || '?'
}"`,
icon: FolderIcon
},
thumbnailer: {
name: 'Generated thumbnails',
name: `Generated ${numberWithCommas(job.task_count)} thumbnails`,
icon: PhotoIcon
},
file_identifier: {
name: 'Identified unique files',
name: `Extracted metadata for ${numberWithCommas(job.task_count)} files`,
icon: EyeIcon
},
object_validator: {
name: `Generated ${numberWithCommas(job.task_count)} full object hashes`,
icon: FingerPrintIcon
}
};
});
const StatusColors: Record<JobReport['status'], string> = {
Running: 'text-blue-500',
@ -41,58 +57,94 @@ function elapsed(seconds: number) {
return new Date(seconds * 1000).toUTCString().match(/(\d\d:\d\d:\d\d)/)?.[0];
}
export function JobsManager() {
const jobs = useLibraryQuery(['jobs.getHistory']);
return (
<div className="h-full">
{/* <div className="z-10 flex flex-row w-full h-10 bg-gray-500 border-b border-gray-700 bg-opacity-30"></div> */}
<div className="h-full mr-1 overflow-x-hidden custom-scroll inspector-scroll">
<div className="py-1 pl-2">
<div className="fixed flex items-center h-10">
<h3 className="mt-1.5 ml-2 text-md font-medium opacity-40">Recent Jobs</h3>
</div>
<div className="h-10"></div>
{jobs.data?.map((job) => {
const color = StatusColors[job.status];
const niceData = NiceData[job.name];
const HeaderContainer = tw.div`z-20 flex items-center w-full h-10 px-2 border-b border-app-line/50 rounded-t-md `;
return (
<div
className="flex items-center px-2 py-2 border-b border-gray-500 bg-opacity-60"
key={job.id}
>
<Tooltip label={job.status}>
<niceData.icon className={clsx('w-5 mr-3', color)} />
</Tooltip>
<div className="flex flex-1 flex-col">
<span className="mt-0.5 font-semibold">{niceData.name}</span>
<div className="flex items-center opacity-60">
<span className="text-xs">
{job.status === 'Failed' ? 'Failed after' : 'Took'}{' '}
{job.seconds_elapsed
? dayjs.duration({ seconds: job.seconds_elapsed }).humanize()
: 'less than a second'}
</span>
<span className="mx-1 opacity-50">&#8226;</span>
<span className="text-xs">{dayjs(job.date_created).toNow(true)} ago</span>
</div>
<span className="text-xs">{job.data}</span>
</div>
<div className="space-x-2">
{job.status === 'Failed' && (
<Button padding="thin" variant="gray">
<ArrowsClockwise className="w-4 h-4" />
</Button>
)}
<Button padding="thin" variant="gray">
<XMarkIcon className="w-4 h-4" />
</Button>
</div>
</div>
);
})}
export function JobsManager() {
const runningJobs = useLibraryQuery(['jobs.getRunning']);
const jobs = useLibraryQuery(['jobs.getHistory']);
return (
<div className="h-full pb-10 overflow-hidden">
<HeaderContainer>
<CategoryHeading className="ml-2">Recent Jobs</CategoryHeading>
<div className="flex-grow" />
<Button size="icon">
<EllipsisHorizontalIcon className="w-5" />
</Button>
</HeaderContainer>
<div className="h-full mr-1 overflow-x-hidden custom-scroll inspector-scroll">
<div className="">
<div className="py-1">
{runningJobs.data?.map((job) => (
<Job key={job.id} job={job} />
))}
{jobs.data?.map((job) => (
<Job key={job.id} job={job} />
))}
</div>
</div>
</div>
</div>
);
}
function Job({ job }: { job: JobReport }) {
const niceData = getNiceData(job)[job.name] || {
name: job.name,
icon: QuestionMarkCircleIcon
};
const isRunning = job.status === 'Running';
return (
<div className="flex items-center px-2 py-2 pl-4 border-b border-app-line/50 bg-opacity-60">
<Tooltip label={job.status}>
<niceData.icon className={clsx('w-5 mr-3')} />
</Tooltip>
<div className="flex flex-col w-full ">
<span className="flex mt-0.5 items-center font-semibold truncate">
{isRunning ? job.message : niceData.name}
</span>
{isRunning && (
<div className="w-full my-1">
<ProgressBar value={job.completed_task_count} total={job.task_count} />
</div>
)}
<div className="flex items-center text-ink-faint">
<span className="text-xs">
{isRunning ? 'Elapsed' : job.status === 'Failed' ? 'Failed after' : 'Took'}{' '}
{job.seconds_elapsed
? dayjs.duration({ seconds: job.seconds_elapsed }).humanize()
: 'less than a second'}
</span>
<span className="mx-1 opacity-50">&#8226;</span>
{
<span className="text-xs">
{isRunning ? 'Unknown time remaining' : dayjs(job.date_created).toNow(true) + ' ago'}
</span>
}
</div>
{/* <span className="mt-0.5 opacity-50 text-tiny text-ink-faint">{job.id}</span> */}
</div>
<div className="flex-grow" />
<div className="flex flex-row space-x-2 ml-7">
{job.status === 'Running' && (
<Button size="icon">
<Pause className="w-4" />
</Button>
)}
{job.status === 'Failed' && (
<Button size="icon">
<ArrowsClockwise className="w-4" />
</Button>
)}
<Button size="icon">
<XMarkIcon className="w-4" />
</Button>
</div>
</div>
);
}
function numberWithCommas(x: number) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

View file

@ -1,83 +0,0 @@
import { Transition } from '@headlessui/react';
import { useLibraryQuery } from '@sd/client';
import clsx from 'clsx';
import { DetailedHTMLProps, HTMLAttributes } from 'react';
import ProgressBar from '../primitive/ProgressBar';
const MiddleTruncatedText = ({
children,
...props
}: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>) => {
const text = children?.toString() ?? '';
const first = text.substring(0, text.length / 2);
const last = text.substring(first.length);
// Literally black magic
const fontFaceScaleFactor = 1.61;
const startWidth = fontFaceScaleFactor * 5;
const endWidth = fontFaceScaleFactor * 4;
return (
<div className="w-full overflow-hidden whitespace-nowrap">
<span
{...props}
style={{
maxWidth: `calc(100% - (1em * ${endWidth}))`,
minWidth: startWidth
}}
className={clsx(
props?.className,
'text-ellipsis inline-block align-bottom whitespace-nowrap overflow-hidden'
)}
>
{first}
</span>
<span
{...props}
style={{
maxWidth: `calc(100% - (1em * ${startWidth}))`,
direction: 'rtl'
}}
className={clsx(
props?.className,
'inline-block align-bottom whitespace-nowrap overflow-hidden'
)}
>
{last}
</span>
</div>
);
};
export default function RunningJobsWidget() {
const { data: jobs } = useLibraryQuery(['jobs.getRunning']);
return (
<div className="flex flex-col space-y-4">
{jobs?.map((job, index) => (
<Transition
key={job.id + index}
show={true}
enter="transition-translate ease-in-out duration-200"
enterFrom="translate-y-24"
enterTo="translate-y-0"
leave="transition-translate ease-in-out duration-200"
leaveFrom="translate-y-0"
leaveTo="translate-y-24"
>
<div
key={job.id}
className="flex flex-col px-2 pt-1.5 pb-2 border bg-gray-600 bg-opacity-50 border-gray-500 rounded"
>
{/* <span className="mb-0.5 text-tiny font-bold text-gray-400">{job.status} Job</span> */}
<MiddleTruncatedText className="mb-1.5 text-gray-450 text-tiny">
{job.message}
</MiddleTruncatedText>
<ProgressBar value={job.completed_task_count} total={job.task_count} />
</div>
</Transition>
))}
</div>
);
}

View file

@ -1,21 +1,7 @@
import { InformationCircleIcon } from '@heroicons/react/24/outline';
import {
EllipsisVerticalIcon,
EyeIcon,
EyeSlashIcon,
KeyIcon,
LockClosedIcon,
LockOpenIcon,
PlusIcon,
TrashIcon,
XMarkIcon
} from '@heroicons/react/24/solid';
import { Button, Input, Select, SelectOption } from '@sd/ui';
import { EllipsisVerticalIcon, EyeIcon, KeyIcon } from '@heroicons/react/24/solid';
import { Button } from '@sd/ui';
import clsx from 'clsx';
import { Eject, EjectSimple, Plus } from 'phosphor-react';
import { useState } from 'react';
import { Toggle } from '../primitive';
import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip';
@ -36,24 +22,17 @@ export interface Key {
}
export const Key: React.FC<{ data: Key; index: number }> = ({ data, index }) => {
const odd = (index || 0) % 2 === 0;
return (
<div
className={clsx(
'flex items-center justify-between px-2 py-1.5 shadow-gray-900/20 text-sm text-gray-300 bg-gray-500/30 shadow-lg border-gray-500 rounded-lg'
// !odd && 'bg-opacity-10'
'flex items-center justify-between px-2 py-1.5 shadow-app-shade/10 text-sm bg-app-box shadow-lg rounded-lg'
)}
>
<div className="flex items-center">
<KeyIcon
className={clsx(
'w-5 h-5 ml-1 mr-3',
data.mounted
? data.locked
? 'text-primary-600'
: 'text-primary-600'
: 'text-gray-400/80'
data.mounted ? (data.locked ? 'text-accent' : 'text-accent') : 'text-gray-400/80'
)}
/>
<div className="flex flex-col ">
@ -69,19 +48,19 @@ export const Key: React.FC<{ data: Key; index: number }> = ({ data, index }) =>
{data.stats ? (
<div className="flex flex-row mt-[1px] space-x-3">
{data.stats.objectCount && (
<div className="text-[8pt] font-medium text-gray-200 opacity-30">
<div className="text-[8pt] font-medium text-ink-dull opacity-30">
{data.stats.objectCount} Objects
</div>
)}
{data.stats.containerCount && (
<div className="text-[8pt] font-medium text-gray-200 opacity-30">
<div className="text-[8pt] font-medium text-ink-dull opacity-30">
{data.stats.containerCount} Containers
</div>
)}
</div>
) : (
!data.mounted && (
<div className="text-[8pt] font-medium text-gray-200 opacity-30">Key not mounted</div>
<div className="text-[8pt] font-medium text-ink-dull opacity-30">Key not mounted</div>
)
)}
</div>
@ -89,13 +68,13 @@ export const Key: React.FC<{ data: Key; index: number }> = ({ data, index }) =>
<div className="space-x-1">
{data.mounted && (
<Tooltip label="Browse files">
<Button padding="thin">
<EyeIcon className="w-4 h-4 text-gray-400" />
<Button size="icon">
<EyeIcon className="w-4 h-4 text-ink-faint" />
</Button>
</Tooltip>
)}
<Button padding="thin">
<EllipsisVerticalIcon className="w-4 h-4 text-gray-400" />
<Button size="icon">
<EllipsisVerticalIcon className="w-4 h-4 text-ink-faint" />
</Button>
</div>
</div>

View file

@ -1,11 +1,6 @@
import { Button, CategoryHeading, Input, Select, SelectOption } from '@sd/ui';
import clsx from 'clsx';
import { Eject, EjectSimple, Plus } from 'phosphor-react';
import { useState } from 'react';
import { Button } from '@sd/ui';
import { Toggle } from '../primitive';
import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip';
import { Key } from './Key';
export type KeyListProps = DefaultProps;
@ -45,7 +40,7 @@ export function KeyList(props: KeyListProps) {
</div>
</div>
</div>
<div className="flex w-full p-2 bg-gray-600 border-t border-gray-500 rounded-b-md">
<div className="flex w-full p-2 border-t border-app-line rounded-b-md">
<Button size="sm" variant="gray">
Unmount All
</Button>

View file

@ -1,30 +1,17 @@
import { InformationCircleIcon } from '@heroicons/react/24/outline';
import {
EllipsisVerticalIcon,
EyeIcon,
EyeSlashIcon,
KeyIcon,
LockClosedIcon,
LockOpenIcon,
PlusIcon,
TrashIcon,
XMarkIcon
} from '@heroicons/react/24/solid';
import { Button, CategoryHeading, Input, Select, SelectOption } from '@sd/ui';
import clsx from 'clsx';
import { Eject, EjectSimple, Plus } from 'phosphor-react';
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/solid';
import { Button, CategoryHeading, Input, Select, SelectOption, Switch, cva, tw } from '@sd/ui';
import { useEffect, useRef, useState } from 'react';
import { Toggle } from '../primitive';
import { DefaultProps } from '../primitive/types';
import { Tooltip } from '../tooltip/Tooltip';
import { Key } from './Key';
const KeyHeading = tw(CategoryHeading)`mb-1`;
export function KeyMounter() {
const ref = useRef<HTMLInputElement>(null);
const [showKey, setShowKey] = useState(false);
const [toggle, setToggle] = useState(false);
const [toggle, setToggle] = useState(true);
const [key, setKey] = useState('');
const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305');
@ -42,7 +29,7 @@ export function KeyMounter() {
return (
<div className="p-3 pt-3 mb-1">
<CategoryHeading>Mount key</CategoryHeading>
<KeyHeading>Mount key</KeyHeading>
<div className="flex space-x-2">
<div className="relative flex flex-grow">
<Input
@ -55,9 +42,8 @@ export function KeyMounter() {
/>
<Button
onClick={() => setShowKey(!showKey)}
noBorder
padding="thin"
className="absolute right-[5px] top-[5px]"
size="icon"
className="border-none absolute right-[5px] top-[5px]"
>
<CurrentEyeIcon className="w-4 h-4" />
</Button>
@ -65,10 +51,17 @@ export function KeyMounter() {
</div>
<div className="flex flex-row items-center mt-3 mb-1">
<Toggle className="dark:bg-gray-400/30" size="sm" value={toggle} onChange={setToggle} />
<span className="ml-3 mt-[1px] font-medium text-xs">Sync with Library</span>
<div className="space-x-2">
<Switch
className="bg-app-selected"
size="sm"
checked={toggle}
onCheckedChange={setToggle}
/>
</div>
<span className="ml-3 text-xs font-medium">Sync with Library</span>
<Tooltip label="This key will be mounted on all devices running your Library">
<InformationCircleIcon className="w-4 h-4 ml-1.5 text-gray-400" />
<InformationCircleIcon className="w-4 h-4 ml-1.5 text-ink-faint" />
</Tooltip>
</div>
@ -84,13 +77,14 @@ export function KeyMounter() {
<span className="text-xs font-bold">Hashing</span>
<Select className="mt-2" onChange={setHashingAlgo} value={hashingAlgo}>
<SelectOption value="Argon2id">Argon2id</SelectOption>
<SelectOption value="Bcrypt">Bcrypt</SelectOption>
</Select>
</div>
</div>
<p className="pt-1.5 ml-0.5 text-[8pt] leading-snug text-gray-300 opacity-50 w-[90%]">
<p className="pt-1.5 ml-0.5 text-[8pt] leading-snug text-ink-faint w-[90%]">
Files encrypted with this key will be revealed and decrypted on the fly.
</p>
<Button className="w-full mt-2" variant="primary">
<Button className="w-full mt-2" variant="accent">
Mount Key
</Button>
</div>

View file

@ -1,15 +0,0 @@
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
export default function Card(props: PropsWithChildren<{ className?: string }>) {
return (
<div
className={clsx(
'flex w-full px-4 py-2 border border-gray-500 rounded-lg bg-gray-550',
props.className
)}
>
{props.children}
</div>
);
}

View file

@ -1,39 +1,389 @@
import { CogIcon, LockClosedIcon, PhotoIcon } from '@heroicons/react/24/outline';
import { CogIcon, LockClosedIcon } from '@heroicons/react/24/outline';
import { PlusIcon } from '@heroicons/react/24/solid';
import { useCurrentLibrary, useLibraryMutation, useLibraryQuery, usePlatform } from '@sd/client';
import { LocationCreateArgs } from '@sd/client';
import { Button, CategoryHeading, Dropdown, OverlayPanel } from '@sd/ui';
import { ReactComponent as Ellipsis } from '@sd/assets/svgs/ellipsis.svg';
import {
LocationCreateArgs,
getDebugState,
useBridgeQuery,
useCurrentLibrary,
useDebugState,
useLibraryMutation,
useLibraryQuery,
usePlatform
} from '@sd/client';
import {
Button,
ButtonLink,
CategoryHeading,
Dropdown,
Loader,
OverlayPanel,
Select,
SelectOption,
Switch,
cva,
tw
} from '@sd/ui';
import clsx from 'clsx';
import { CheckCircle, CirclesFour, Planet, WaveTriangle } from 'phosphor-react';
import { PropsWithChildren } from 'react';
import { NavLink, NavLinkProps, useNavigate } from 'react-router-dom';
import { CheckCircle, CirclesFour, Planet, ShareNetwork } from 'phosphor-react';
import React, { PropsWithChildren, useState } from 'react';
import { NavLink, NavLinkProps } from 'react-router-dom';
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
import CreateLibraryDialog from '../dialog/CreateLibraryDialog';
import { Folder } from '../icons/Folder';
import { JobsManager } from '../jobs/JobManager';
import RunningJobsWidget from '../jobs/RunningJobsWidget';
import { MacTrafficLights } from '../os/TrafficLights';
import { InputContainer } from '../primitive/InputContainer';
export const SidebarLink = (props: PropsWithChildren<NavLinkProps>) => (
<NavLink {...props}>
{({ isActive }) => (
<span
className={clsx(
'max-w mb-[2px] text-gray-550 dark:text-gray-300 rounded px-2 py-1 flex flex-row flex-grow items-center font-medium text-sm',
{
'!bg-gray-400 !bg-opacity-10 !text-white hover:bg-gray-400 dark:hover:bg-gray-400':
isActive
},
props.className
)}
export function Sidebar() {
const os = useOperatingSystem();
const { library, libraries, isLoading: isLoadingLibraries, switchLibrary } = useCurrentLibrary();
const debugState = useDebugState();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const { data: isRunningJob } = useLibraryQuery(['jobs.isRunning']);
// const itemStyles = macOnly(os, 'dark:hover:bg-sidebar-box dark:hover:bg-opacity-50');
return (
<div
className={clsx(
'flex relative flex-col flex-grow-0 flex-shrink-0 w-44 min-h-full border-r border-sidebar-divider bg-sidebar',
macOnly(os, 'bg-opacity-[0.80]')
)}
>
<WindowControls />
<Dropdown.Root
className="mt-1 mx-2.5"
button={
<Dropdown.Button
variant="gray"
className={clsx(
`w-full text-ink !bg-sidebar-box !border-sidebar-line/50 active:!border-sidebar-line`,
`active:!bg-sidebar-button ui-open:!bg-sidebar-button ui-open:!border-sidebar-line `,
(library === null || isLoadingLibraries) && '!text-ink-faint'
// macOnly(os, '!bg-opacity-80 !border-opacity-40')
)}
>
<span className="truncate">
{isLoadingLibraries ? 'Loading...' : library ? library.config.name : ' '}
</span>
</Dropdown.Button>
}
>
{props.children}
</span>
)}
</NavLink>
<Dropdown.Section>
{libraries?.map((lib) => (
<Dropdown.Item
selected={lib.uuid === library?.uuid}
key={lib.uuid}
onClick={() => switchLibrary(lib.uuid)}
>
{lib.config.name}
</Dropdown.Item>
))}
</Dropdown.Section>
<Dropdown.Section>
<Dropdown.Item icon={CogIcon} to="settings/library">
Library Settings
</Dropdown.Item>
<Dropdown.Item icon={PlusIcon} onClick={() => setIsCreateDialogOpen(true)}>
Add Library
</Dropdown.Item>
<Dropdown.Item icon={LockClosedIcon} onClick={() => alert('TODO: Not implemented yet!')}>
Lock
</Dropdown.Item>
</Dropdown.Section>
</Dropdown.Root>
<div className="flex flex-col px-2.5 flex-grow pt-1 pb-10 overflow-x-hidden overflow-y-scroll no-scrollbar mask-fade-out">
<div className="pt-1">
<SidebarLink to="/overview">
<Icon component={Planet} />
Overview
</SidebarLink>
<SidebarLink to="photos">
<Icon component={ShareNetwork} />
Nodes
</SidebarLink>
<SidebarLink to="content">
<Icon component={CirclesFour} />
Spaces
</SidebarLink>
</div>
{library && <LibraryScopedSection />}
<div className="flex-grow" />
</div>
{/* <div className="fixed w-[174px] bottom-[2px] left-[2px] h-20 rounded-[8px] bg-gradient-to-t from-sidebar-box/90 via-sidebar-box/50 to-transparent" /> */}
<div className="flex flex-col mb-3 px-2.5">
<div className="flex">
<ButtonLink to="/settings/general" size="icon" variant="outline">
<CogIcon className="w-5 h-5" />
</ButtonLink>
<OverlayPanel
className="focus:outline-none"
transformOrigin="bottom left"
disabled={!library}
trigger={
<Button
size="icon"
variant="outline"
className="radix-state-open:bg-sidebar-selected/50"
>
{isRunningJob ? (
<Loader className="w-[20px] h-[20px]" />
) : (
<CheckCircle className="w-5 h-5" />
)}
</Button>
}
>
<div className="block w-[430px] h-96">
<JobsManager />
</div>
</OverlayPanel>
</div>
{debugState.enabled && <DebugPanel />}
</div>
{/* Putting this within the dropdown will break the enter click handling in the modal. */}
<CreateLibraryDialog open={isCreateDialogOpen} setOpen={setIsCreateDialogOpen} />
</div>
);
}
function DebugPanel() {
const buildInfo = useBridgeQuery(['buildInfo']);
const nodeState = useBridgeQuery(['nodeState']);
const debugState = useDebugState();
const platform = usePlatform();
return (
<OverlayPanel
className="p-4 focus:outline-none"
transformOrigin="bottom left"
trigger={
<h1 className="w-full ml-1 mt-1 text-[7pt] text-ink-faint/50">
v{buildInfo.data?.version || '-.-.-'} - {buildInfo.data?.commit || 'dev'}
</h1>
}
>
<div className="block w-[430px] h-96">
<InputContainer
mini
title="rspc Logger"
description="Enable the logger link so you can see what's going on in the browser logs."
>
<Switch
checked={debugState.rspcLogger}
onClick={() => (getDebugState().rspcLogger = !debugState.rspcLogger)}
/>
</InputContainer>
{platform.openPath && (
<InputContainer
mini
title="Open Data Directory"
description="Quickly get to your Spacedrive database"
>
<div className="mt-2">
<Button
size="sm"
variant="gray"
onClick={() => {
if (nodeState?.data?.data_path) platform.openPath!(nodeState?.data?.data_path);
}}
>
Open
</Button>
</div>
</InputContainer>
)}
<InputContainer
mini
title="React Query Devtools"
description="Configure the React Query devtools."
>
<Select
value={debugState.reactQueryDevtools}
size="sm"
onChange={(value) => (getDebugState().reactQueryDevtools = value as any)}
>
<SelectOption value="disabled">Disabled</SelectOption>
<SelectOption value="invisible">Invisble</SelectOption>
<SelectOption value="enabled">Enabled</SelectOption>
</Select>
</InputContainer>
{/* {platform.showDevtools && (
<InputContainer
mini
title="Devtools"
description="Allow opening browser devtools in a production build"
>
<div className="mt-2">
<Button size="sm" variant="gray" onClick={platform.showDevtools}>
Show
</Button>
</div>
</InputContainer>
)} */}
</div>
</OverlayPanel>
);
}
const sidebarItemClass = cva(
'max-w mb-[2px] rounded px-2 py-1 gap-0.5 flex flex-row flex-grow items-center font-medium truncate text-sm',
{
variants: {
isActive: {
true: 'bg-sidebar-selected/40 text-ink',
false: 'text-ink-dull'
},
isTransparent: {
true: 'bg-opacity-90',
false: ''
}
}
}
);
export const SidebarLink = (props: PropsWithChildren<NavLinkProps>) => {
const os = useOperatingSystem();
return (
<NavLink {...props}>
{({ isActive }) => (
<span
className={clsx(
sidebarItemClass({ isActive, isTransparent: os === 'macOS' }),
props.className
)}
>
{props.children}
</span>
)}
</NavLink>
);
};
const SidebarSection: React.FC<{
name: string;
actionArea?: React.ReactNode;
children: React.ReactNode;
}> = (props) => {
return (
<div className="mt-5 group">
<div className="flex items-center justify-between mb-1">
<CategoryHeading className="ml-1">{props.name}</CategoryHeading>
<div className="transition-all duration-300 opacity-0 text-ink-faint group-hover:opacity-30 hover:!opacity-100">
{props.actionArea}
</div>
</div>
{props.children}
</div>
);
};
const SidebarHeadingOptionsButton: React.FC<{ to: string; icon?: React.FC }> = (props) => {
const Icon = props.icon ?? Ellipsis;
return (
<NavLink to={props.to}>
<Button className="!p-[5px]" variant="outline">
<Icon className="w-3 h-3" />
</Button>
</NavLink>
);
};
function LibraryScopedSection() {
const platform = usePlatform();
const { data: locations } = useLibraryQuery(['locations.list'], { keepPreviousData: true });
const { data: tags } = useLibraryQuery(['tags.list'], { keepPreviousData: true });
const { mutate: createLocation } = useLibraryMutation('locations.create');
return (
<>
<div>
<SidebarSection
name="Locations"
actionArea={
<>
{/* <SidebarHeadingOptionsButton to="/settings/locations" icon={CogIcon} /> */}
<SidebarHeadingOptionsButton to="/settings/locations" />
</>
}
>
{locations?.map((location) => {
return (
<div key={location.id} className="flex flex-row items-center">
<NavLink
className="relative w-full group"
to={{
pathname: `location/${location.id}`
}}
>
{({ isActive }) => (
<span className={sidebarItemClass({ isActive })}>
<div className="-mt-0.5 mr-1 flex-grow-0 flex-shrink-0">
<Folder size={18} />
</div>
<span className="flex-grow flex-shrink-0">{location.name}</span>
</span>
)}
</NavLink>
</div>
);
})}
{(locations?.length || 0) < 4 && (
<button
onClick={() => {
if (!platform.openFilePickerDialog) {
// TODO: Support opening locations on web
alert('Opening a dialogue is not supported on this platform!');
return;
}
platform.openFilePickerDialog().then((result) => {
// TODO: Pass indexer rules ids to create location
if (result)
createLocation({
path: result as string,
indexer_rules_ids: []
} as LocationCreateArgs);
});
}}
className={clsx(
'w-full px-2 py-1 mt-1 text-xs font-medium text-center',
'rounded border border-dashed border-sidebar-line hover:border-sidebar-selected',
'cursor-normal transition text-ink-faint'
)}
>
Add Location
</button>
)}
</SidebarSection>
</div>
{!!tags?.length && (
<SidebarSection
name="Tags"
actionArea={<SidebarHeadingOptionsButton to="/settings/tags" />}
>
<div className="mt-1 mb-2">
{tags?.slice(0, 6).map((tag, index) => (
<SidebarLink key={index} to={`tag/${tag.id}`} className="">
<div
className="w-[12px] h-[12px] rounded-full"
style={{ backgroundColor: tag.color || '#efefef' }}
/>
<span className="ml-1.5 text-sm">{tag.name}</span>
</SidebarLink>
))}
</div>
</SidebarSection>
)}
</>
);
}
const Icon = ({ component: Icon, ...props }: any) => (
<Icon weight="bold" {...props} className={clsx('w-4 h-4 mr-2', props.className)} />
);
@ -57,210 +407,3 @@ function WindowControls() {
return null;
}
function LibraryScopedSection() {
const os = useOperatingSystem();
const platform = usePlatform();
const { data: locations } = useLibraryQuery(['locations.list'], { keepPreviousData: true });
const { data: tags } = useLibraryQuery(['tags.list'], { keepPreviousData: true });
const { mutate: createLocation } = useLibraryMutation('locations.create');
return (
<>
<div>
<CategoryHeading className="mt-5">Locations</CategoryHeading>
{locations?.map((location) => {
return (
<div key={location.id} className="flex flex-row items-center">
<NavLink
className="relative w-full group"
to={{
pathname: `location/${location.id}`
}}
>
{({ isActive }) => (
<span
className={clsx(
'max-w mb-[2px] text-gray-550 dark:text-gray-150 rounded px-2 py-1 gap-2 flex flex-row flex-grow items-center truncate text-sm',
{
'!bg-gray-400 !bg-opacity-10 !text-white hover:bg-gray-400 dark:hover:bg-gray-400':
isActive
}
)}
>
<div className="-mt-0.5 flex-grow-0 flex-shrink-0">
{/* <Folder size={18} className={clsx(!isActive && 'hidden')} white /> */}
<Folder size={18} />
</div>
<span className="flex-grow flex-shrink-0">{location.name}</span>
</span>
)}
</NavLink>
</div>
);
})}
{(locations?.length || 0) < 1 && (
<button
onClick={() => {
if (!platform.openFilePickerDialog) {
// TODO: Support opening locations on web
alert('Opening a dialogue is not supported on this platform!');
return;
}
platform.openFilePickerDialog().then((result) => {
// TODO: Pass indexer rules ids to create location
if (result)
createLocation({
path: result as string,
indexer_rules_ids: []
} as LocationCreateArgs);
});
}}
className={clsx(
'w-full px-2 py-1.5 mt-1 text-xs font-bold text-center text-gray-400 border border-dashed rounded border-transparent cursor-normal border-gray-350 transition',
os === 'macOS'
? 'dark:text-gray-450 dark:border-gray-450 hover:dark:border-gray-400 dark:border-opacity-60'
: 'dark:text-gray-450 dark:border-gray-550 hover:dark:border-gray-500'
)}
>
Add Location
</button>
)}
</div>
{tags?.length ? (
<div>
<CategoryHeading className="mt-5">Tags</CategoryHeading>
<div className="mb-2">
{tags?.slice(0, 6).map((tag, index) => (
<SidebarLink key={index} to={`tag/${tag.id}`} className="">
<div
className="w-[12px] h-[12px] rounded-full"
style={{ backgroundColor: tag.color || '#efefef' }}
/>
<span className="ml-2 text-sm">{tag.name}</span>
</SidebarLink>
))}
</div>
</div>
) : (
<></>
)}
</>
);
}
export function Sidebar() {
const navigate = useNavigate();
const os = useOperatingSystem();
const { library, libraries, isLoading: isLoadingLibraries, switchLibrary } = useCurrentLibrary();
return (
<div
className={clsx(
'flex flex-col flex-grow-0 flex-shrink-0 w-48 min-h-full px-2.5 overflow-x-hidden overflow-y-scroll border-r border-gray-100 no-scrollbar bg-gray-50 dark:bg-gray-850 dark:border-gray-750',
macOnly(os, 'dark:!bg-opacity-30')
)}
>
<WindowControls />
<Dropdown
buttonProps={{
justify: 'left',
className: clsx(
`flex w-full text-left max-w-full mb-1 mt-1 -mr-0.5 shadow-xs rounded !bg-gray-50 border-gray-150 hover:!bg-gray-1000 dark:!bg-gray-500 dark:hover:!bg-gray-500 dark:!border-gray-550 dark:hover:!border-gray-500`,
macOnly(
os,
'dark:!bg-opacity-40 dark:hover:!bg-opacity-70 dark:!border-[#333949] dark:hover:!border-[#394052]'
)
),
variant: 'gray'
}}
// to support the transparent sidebar on macOS we use slightly adjusted styles
itemsClassName={macOnly(os, 'dark:bg-gray-800 dark:divide-gray-600')}
itemButtonClassName={macOnly(os, 'dark:hover:bg-gray-550 dark:hover:bg-opacity-50')}
// this shouldn't default to "My Library", it is only this way for landing demo
buttonText={isLoadingLibraries ? 'Loading...' : library ? library.config.name : ' '}
buttonTextClassName={library === null || isLoadingLibraries ? 'text-gray-300' : undefined}
items={[
libraries?.map((lib) => ({
name: lib.config.name,
selected: lib.uuid === library?.uuid,
onPress: () => switchLibrary(lib.uuid)
})) || [],
[
{
name: 'Library Settings',
icon: CogIcon,
to: 'settings/library'
},
{
name: 'Add Library',
icon: PlusIcon,
wrapItemComponent: CreateLibraryDialog
},
{
name: 'Lock',
icon: LockClosedIcon,
disabled: true,
onPress: () => {
alert('TODO: Not implemented yet!');
}
}
]
]}
/>
<div className="pt-1">
<SidebarLink to="/overview">
<Icon component={Planet} />
Overview
</SidebarLink>
<SidebarLink to="content">
<Icon component={CirclesFour} />
Spaces
</SidebarLink>
<SidebarLink to="photos">
<Icon component={PhotoIcon} />
Photos
</SidebarLink>
</div>
{library && <LibraryScopedSection />}
<div className="flex-grow" />
{library && <RunningJobsWidget />}
<div className="mt-2 mb-3">
<NavLink to="/settings/general">
{({ isActive }) => (
<Button padding="sm" variant="default" className={clsx('hover:!bg-opacity-20')}>
<CogIcon className="w-5 h-5" />
</Button>
)}
</NavLink>
<OverlayPanel
className="focus:outline-none"
transformOrigin="bottom left"
disabled={!library}
trigger={
<Button
padding="sm"
className={clsx(
'!outline-none hover:!bg-opacity-20 disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
<CheckCircle className="w-5 h-5" />
</Button>
}
>
<div className="block w-[500px] h-96">
<JobsManager />
</div>
</OverlayPanel>
</div>
</div>
);
}

View file

@ -1,7 +1,7 @@
import { TrashIcon } from '@heroicons/react/24/solid';
import { useLibraryMutation } from '@sd/client';
import { Location, Node } from '@sd/client';
import { Button, Dialog } from '@sd/ui';
import { Button, Card, Dialog } from '@sd/ui';
import clsx from 'clsx';
import { Repeat } from 'phosphor-react';
import { useState } from 'react';
@ -14,6 +14,7 @@ interface LocationListItemProps {
export default function LocationListItem({ location }: LocationListItemProps) {
const [hide, setHide] = useState(false);
const [open, setOpen] = useState(false);
const { mutate: locRescan } = useLibraryMutation('locations.fullRescan');
@ -29,31 +30,32 @@ export default function LocationListItem({ location }: LocationListItemProps) {
if (hide) return <></>;
return (
<div className="flex w-full px-4 py-2 border border-gray-500 rounded-lg bg-gray-550">
<Card>
<Folder size={30} className="mr-3" />
<div className="flex flex-col">
<div className="grid grid-cols-1 min-w-[110px]">
<h1 className="pt-0.5 text-sm font-semibold">{location.name}</h1>
<p className="mt-0.5 text-sm select-text text-gray-250">
<span className="py-[1px] px-1 bg-gray-500 rounded mr-1">{location.node.name}</span>
<p className="mt-0.5 text-sm truncate select-text text-ink-dull">
<span className="py-[1px] px-1 bg-app-selected rounded mr-1">{location.node.name}</span>
{location.local_path}
</p>
</div>
<div className="flex flex-grow" />
<div className="flex h-[45px] p-2 space-x-2">
<Button disabled variant="gray" className="!py-1.5 !px-2 pointer-events-none flex">
<>
<div
className={clsx(
'w-2 h-2 rounded-full',
location.is_online ? 'bg-green-500' : 'bg-red-500'
)}
/>
<span className="ml-1.5 text-xs text-gray-350">
{location.is_online ? 'Online' : 'Offline'}
</span>
</>
{/* This is a fake button, do not add disabled prop pls */}
<Button variant="gray" className="!py-1.5 !px-2 pointer-events-none flex">
<div
className={clsx(
'w-2 h-2 rounded-full',
location.is_online ? 'bg-green-500' : 'bg-red-500'
)}
/>
<span className="ml-1.5 text-xs text-ink-dull">
{location.is_online ? 'Online' : 'Offline'}
</span>
</Button>
<Dialog
open={open}
setOpen={setOpen}
title="Delete Location"
description="Deleting a location will also remove all files associated with it from the Spacedrive database, the files themselves will not be deleted."
ctaAction={() => {
@ -82,6 +84,6 @@ export default function LocationListItem({ location }: LocationListItemProps) {
<CogIcon className="w-4 h-4" />
</Button> */}
</div>
</div>
</Card>
);
}

View file

@ -1,16 +1,28 @@
import clsx from 'clsx';
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { Button } from '../../../../ui/src';
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
import CreateLibraryDialog from '../dialog/CreateLibraryDialog';
// TODO: This page requires styling for now it is just a placeholder.
export default function OnboardingPage() {
const os = useOperatingSystem();
const navigate = useNavigate();
const [open, setOpen] = useState(false);
return (
<div className="p-10 flex flex-col justify-center">
<div
className={clsx(
'h-screen p-10 flex flex-col justify-center',
os !== 'macOS' && 'bg-white dark:bg-black'
)}
>
<h1 className="text-red-500">Welcome to Spacedrive</h1>
<CreateLibraryDialog onSubmit={() => navigate('overview')}>
<Button variant="primary" size="sm">
<CreateLibraryDialog open={open} setOpen={setOpen} onSubmit={() => navigate('/overview')}>
<Button variant="accent" size="sm">
Create your library
</Button>
</CreateLibraryDialog>

View file

@ -9,18 +9,15 @@ interface InputContainerProps extends DefaultProps<HTMLDivElement> {
mini?: boolean;
}
export function InputContainer(props: PropsWithChildren<InputContainerProps>) {
export function InputContainer({ mini, ...props }: PropsWithChildren<InputContainerProps>) {
return (
<div className="flex flex-row">
<div
className={clsx('flex flex-col w-full', !props.mini && 'pb-6', props.className)}
{...props}
>
<div {...props} className={clsx('flex flex-col w-full', !mini && 'pb-6', props.className)}>
<h3 className="mb-1 text-sm font-medium text-gray-700 dark:text-gray-100">{props.title}</h3>
{!!props.description && <p className="mb-2 text-sm text-gray-400 ">{props.description}</p>}
{!props.mini && props.children}
{!mini && props.children}
</div>
{props.mini && props.children}
{mini && props.children}
</div>
);
}

View file

@ -55,7 +55,7 @@ export default function Listbox(props: { options: ListboxOption[]; className?: s
className={({ active }) =>
`cursor-default select-none relative rounded m-1 py-2 pl-8 pr-4 dark:text-white focus:outline-none ${
active
? 'text-primary-900 bg-primary-600'
? 'text-accent bg-accent'
: 'text-gray-900 dark:hover:bg-gray-600 dark:hover:bg-opacity-20'
}`
}

View file

@ -14,7 +14,7 @@ const ProgressBar = (props: Props) => {
>
<ProgressPrimitive.Indicator
style={{ width: `${percentage}%` }}
className="h-full duration-300 ease-in-out bg-primary "
className="h-full duration-300 ease-in-out bg-accent "
/>
</ProgressPrimitive.Root>
);

View file

@ -13,9 +13,8 @@ export const Shortcut: React.FC<ShortcutProps> = (props) => {
<kbd
className={clsx(
`px-1 border border-b-2`,
`rounded-lg text-xs font-bold`,
`text-gray-400 bg-gray-200 border-gray-300`,
`dark:text-gray-400 dark:bg-transparent dark:border-transparent`,
`rounded-md text-xs font-bold`,
`border-app-line dark:border-transparent`,
className
)}
{...rest}

View file

@ -6,11 +6,11 @@ const Slider = (props: SliderPrimitive.SliderProps) => (
{...props}
className={clsx('relative flex items-center w-full h-6 select-none', props.className)}
>
<SliderPrimitive.Track className="relative flex-grow h-2 bg-gray-500 rounded-full outline-none">
<SliderPrimitive.Range className="absolute h-full rounded-full outline-none bg-primary-500" />
<SliderPrimitive.Track className="relative flex-grow h-2 rounded-full outline-none bg-app-box">
<SliderPrimitive.Range className="absolute h-full rounded-full outline-none bg-accent" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb
className="z-50 block w-5 h-5 font-bold transition rounded-full shadow-lg outline-none shadow-black/50 bg-primary-500 ring-primary-500 ring-opacity-30 focus:ring-4"
className="z-50 block w-5 h-5 font-bold transition rounded-full shadow-lg outline-none shadow-black/20 bg-accent ring-accent ring-opacity-30 focus:ring-4"
data-tip="1.0"
/>
</SliderPrimitive.Root>

View file

@ -0,0 +1,75 @@
import * as ToastPrimitive from '@radix-ui/react-toast';
import clsx from 'clsx';
import { useToasts } from '../../hooks/useToasts';
export function Toasts() {
const { toasts, addToast, removeToast } = useToasts();
return (
<div className="fixed right-0 flex">
<ToastPrimitive.Provider>
<>
{toasts.map((toast) => (
<ToastPrimitive.Root
key={toast.id}
open={true}
onOpenChange={() => removeToast(toast)}
duration={toast.duration || 3000}
className={clsx(
'w-80 m-4 shadow-lg rounded-lg',
'bg-app-box/20 backdrop-blur',
'radix-state-open:animate-toast-slide-in-bottom md:radix-state-open:animate-toast-slide-in-right',
'radix-state-closed:animate-toast-hide',
'radix-swipe-end:animate-toast-swipe-out',
'translate-x-radix-toast-swipe-move-x',
'radix-swipe-cancel:translate-x-0 radix-swipe-cancel:duration-200 radix-swipe-cancel:ease-[ease]',
'focus:outline-none focus-visible:ring focus-visible:ring-accent focus-visible:ring-opacity-75 border-white/10 border-2 shadow-2xl'
)}
>
<div className="flex">
<div className="flex items-center flex-1 w-0 py-4 pl-5">
<div className="w-full radix">
<ToastPrimitive.Title className="text-sm font-medium text-black">
{toast.title}
</ToastPrimitive.Title>
{toast.subtitle && (
<ToastPrimitive.Description className="mt-1 text-sm text-black">
{toast.subtitle}
</ToastPrimitive.Description>
)}
</div>
</div>
<div className="flex">
<div className="flex flex-col px-3 py-2 space-y-1">
<div className="flex flex-1 h-0">
{toast.actionButton && (
<ToastPrimitive.Action
altText="view now"
className="flex items-center justify-center w-full px-3 py-2 text-sm font-medium border border-transparent rounded-lg text-accent hover:bg-white/10 focus:z-10 focus:outline-none focus-visible:ring focus-visible:ring-accent focus-visible:ring-opacity-75"
onClick={(e) => {
e.preventDefault();
toast.actionButton?.onClick();
removeToast(toast);
}}
>
{toast.actionButton.text || 'Open'}
</ToastPrimitive.Action>
)}
</div>
<div className="flex flex-1 h-0">
<ToastPrimitive.Close className="flex items-center justify-center w-full px-3 py-2 text-sm font-medium border border-transparent rounded-lg text-ink-faint hover:bg-white/10 focus:z-10 focus:outline-none focus-visible:ring focus-visible:ring-accent focus-visible:ring-opacity-75">
Dismiss
</ToastPrimitive.Close>
</div>
</div>
</div>
</div>
</ToastPrimitive.Root>
))}
<ToastPrimitive.Viewport />
</>
</ToastPrimitive.Provider>
</div>
);
}

View file

@ -1,42 +0,0 @@
import { Switch } from '@headlessui/react';
import clsx from 'clsx';
export interface ToggleProps {
value: boolean;
onChange?: (newValue: boolean) => void;
size?: 'sm' | 'md';
className?: string;
}
export const Toggle: React.FC<ToggleProps> = (props) => {
const { value: isEnabled = false, onChange = (val) => null, size = 'sm' } = props;
return (
<Switch
checked={isEnabled}
onChange={onChange}
className={clsx(
'transition relative flex-shrink-0 inline-flex items-center h-6 w-11 rounded-full bg-gray-200 dark:bg-gray-550',
props.className,
{
'!bg-primary-500 dark:!bg-primary-500': isEnabled,
'h-[20px] w-[35px]': size === 'sm',
'h-8 w-[55px]': size === 'md'
}
)}
>
<span
className={clsx(
'transition inline-block w-4 h-4 transform bg-white rounded-full',
isEnabled ? 'translate-x-6' : 'translate-x-1',
{
'w-3 h-3': size === 'sm',
'h-6 w-6': size === 'md',
'translate-x-5': size === 'sm' && isEnabled,
'translate-x-7': size === 'md' && isEnabled
}
)}
/>
</Switch>
);
};

View file

@ -1,31 +0,0 @@
interface StyleState {
active: string[];
hover: string[];
normal: string[];
}
interface Variant {
base: string;
light: StyleState;
dark: StyleState;
}
function tw(variant: Variant): string {
return `${variant.base} ${variant.light}`;
}
const variants: Record<string, string> = {
default: tw({
base: 'shadow-sm',
light: {
normal: ['bg-gray-50', 'border-gray-100', 'text-gray-700'],
hover: ['bg-gray-100', 'border-gray-200', 'text-gray-900'],
active: ['bg-gray-50', 'border-gray-200', 'text-gray-600']
},
dark: {
normal: ['bg-gray-800 ', 'border-gray-100', ' text-gray-200'],
active: ['bg-gray-700 ', 'border-gray-200 ', 'text-white'],
hover: ['bg-gray-700 ', 'border-gray-600 ', 'text-white']
}
})
};

View file

@ -1 +0,0 @@
export * from './Toggle';

View file

@ -15,7 +15,7 @@ import { SettingsHeading, SettingsIcon } from './SettingsHeader';
export const SettingsSidebar = () => {
return (
<div className="h-full border-r max-w-[200px] flex-shrink-0 border-gray-100 w-60 dark:border-gray-550">
<div className="h-full border-r max-w-[180px] flex-shrink-0 border-app-line/50 w-60 custom-scroll no-scrollbar pb-5">
<div data-tauri-drag-region className="w-full h-7" />
<div className="px-4 py-2.5">
<SettingsHeading className="!mt-0">Client</SettingsHeading>
@ -37,7 +37,7 @@ export const SettingsSidebar = () => {
</SidebarLink>
<SidebarLink to="/settings/keybindings">
<SettingsIcon component={KeyReturn} />
Keybindings
Keybinds
</SidebarLink>
<SidebarLink to="/settings/extensions">
<SettingsIcon component={PuzzlePiece} />

View file

@ -0,0 +1,27 @@
import { useCurrentLibrary } from '@sd/client';
import { useEffect } from 'react';
import { FieldValues, UseFormReturn } from 'react-hook-form';
import { useDebouncedCallback } from 'use-debounce';
export function useDebouncedForm<TFieldValues extends FieldValues = FieldValues, TContext = any>(
form: UseFormReturn<{ id: string } & object, TContext>,
callback: (data: any) => void,
args?: { disableResetOnLibraryChange?: boolean }
) {
const { library } = useCurrentLibrary();
const debounced = useDebouncedCallback(callback, 500);
// listen for any form changes
form.watch(debounced);
// persist unchanged data when the component is unmounted
useEffect(() => () => debounced.flush(), [debounced]);
// ensure the form is updated when the library changes
useEffect(() => {
if (args?.disableResetOnLibraryChange !== true && library?.uuid !== form.getValues('id')) {
form.reset({ id: library?.uuid, ...library?.config });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [library, form.getValues, form.reset, args?.disableResetOnLibraryChange]);
}

View file

@ -1,4 +1,12 @@
export default function ContentScreen() {
// const [address, setAddress] = React.useState('');
return <div className="flex flex-col w-full h-screen p-5 custom-scroll page-scroll"></div>;
return (
<div className="flex flex-col w-full h-screen p-5 custom-scroll page-scroll app-background">
<div className="flex flex-col space-y-5 pb-7">
<p className="px-5 py-3 mb-3 text-sm border rounded-md shadow-sm border-app-line bg-app-box ">
<b>Note: </b>This is a pre-alpha build of Spacedrive, many features are yet to be
functional.
</p>
</div>
</div>
);
}

View file

@ -5,7 +5,7 @@ import CodeBlock from '../components/primitive/Codeblock';
// TODO: Bring this back with a button in the sidebar near settings at the bottom
export default function DebugScreen() {
const platform = usePlatform();
const { data: nodeState } = useBridgeQuery(['getNode']);
const { data: nodeState } = useBridgeQuery(['nodeState']);
const { data: libraryState } = useBridgeQuery(['library.list']);
const { data: jobs } = useLibraryQuery(['jobs.getRunning']);
const { data: jobHistory } = useLibraryQuery(['jobs.getHistory']);
@ -16,7 +16,7 @@ export default function DebugScreen() {
// });
const { mutate: identifyFiles } = useLibraryMutation('jobs.identifyUniqueFiles');
return (
<div className="flex flex-col w-full h-screen custom-scroll page-scroll">
<div className="flex flex-col w-full h-screen custom-scroll page-scroll app-background">
<div data-tauri-drag-region className="flex flex-shrink-0 w-full h-5" />
<div className="flex flex-col p-5 pt-2 space-y-5 pb-7">
<h1 className="text-lg font-bold ">Developer Debugger</h1>

View file

@ -1,5 +1,6 @@
import { PlusIcon } from '@heroicons/react/24/solid';
import {
getExplorerStore,
onLibraryChange,
queryClient,
useCurrentLibrary,
@ -7,8 +8,6 @@ import {
usePlatform
} from '@sd/client';
import { Statistics } from '@sd/client';
import { Button, Input } from '@sd/ui';
import { Dialog } from '@sd/ui';
import byteSize from 'byte-size';
import clsx from 'clsx';
import { useEffect } from 'react';
@ -92,7 +91,7 @@ const StatItem: React.FC<StatItemProps> = (props) => {
return (
<div
className={clsx(
'flex flex-col flex-shrink-0 w-32 px-4 py-3 duration-75 transform rounded-md cursor-default hover:bg-gray-50 hover:dark:bg-gray-600',
'flex flex-col flex-shrink-0 w-32 px-4 py-3 duration-75 transform rounded-md cursor-default ',
!+bytes && 'hidden'
)}
>
@ -136,10 +135,8 @@ export default function OverviewScreen() {
}
);
console.log(overviewStats);
return (
<div className="flex flex-col w-full h-screen overflow-x-hidden custom-scroll page-scroll">
<div className="flex flex-col w-full h-screen overflow-x-hidden custom-scroll page-scroll app-background">
<div data-tauri-drag-region className="flex flex-shrink-0 w-full h-5" />
{/* PAGE */}
@ -165,17 +162,14 @@ export default function OverviewScreen() {
<div className="flex-grow" />
<div className="flex items-center h-full space-x-2">
<div>
<Dialog
{/* <Dialog
title="Add Device"
description="Connect a new device to your library. Either enter another device's code or copy this one."
// ctaAction={() => {}}
ctaLabel="Connect"
trigger={
<Button
size="sm"
icon={<PlusIcon className="inline w-4 h-4 -mt-0.5 xl:mr-1" />}
variant="gray"
>
<Button size="sm" variant="gray">
<PlusIcon className="inline w-4 h-4 -mt-0.5 xl:mr-1" />
<span className="hidden xl:inline-block">Add Device</span>
</Button>
}
@ -194,7 +188,7 @@ export default function OverviewScreen() {
<Input value="" />
</div>
</div>
</Dialog>
</Dialog>*/}
</div>
</div>
</div>

View file

@ -1,11 +1,19 @@
import Spline from '@splinetool/react-spline';
export default function PhotosScreen() {
return (
<div className="flex flex-col w-full h-screen p-5 custom-scroll page-scroll">
<div className="flex flex-col w-full h-screen p-5 custom-scroll page-scroll app-background">
<div className="flex flex-col space-y-5 pb-7">
<p className="px-5 py-3 mb-3 text-sm text-gray-400 rounded-md bg-gray-50 dark:text-gray-400 dark:bg-gray-600">
<p className="px-5 py-3 mb-3 text-sm border rounded-md shadow-sm border-app-line bg-app-box ">
<b>Note: </b>This is a pre-alpha build of Spacedrive, many features are yet to be
functional.
</p>
{/* <Spline
style={{ height: 500 }}
height={500}
className="rounded-md shadow-sm pointer-events-auto"
scene="https://prod.spline.design/KUmO4nOh8IizEiCx/scene.splinecode"
/> */}
</div>
</div>
);

View file

@ -5,7 +5,7 @@ import { SettingsSidebar } from '../../components/settings/SettingsSidebar';
export default function SettingsScreen() {
return (
<div className="flex flex-row w-full">
<div className="flex flex-row w-full app-background">
<SettingsSidebar />
<div className="w-full">
<div data-tauri-drag-region className="w-full h-7" />

View file

@ -1,6 +1,6 @@
import { Switch } from '@sd/ui';
import { useState } from 'react';
import { Toggle } from '../../../components/primitive';
import { InputContainer } from '../../../components/primitive/InputContainer';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
@ -17,14 +17,14 @@ export default function AppearanceSettings() {
title="UI Animations"
description="Dialogs and other UI elements will animate when opening and closing."
>
<Toggle value={uiAnimations} onChange={setUiAnimations} className="m-2 ml-4" />
<Switch checked={uiAnimations} onCheckedChange={setUiAnimations} className="m-2 ml-4" />
</InputContainer>
<InputContainer
mini
title="Blur Effects"
description="Some components will have a blur effect applied to them."
>
<Toggle value={blurEffects} onChange={setBlurEffects} className="m-2 ml-4" />
<Switch checked={blurEffects} onCheckedChange={setBlurEffects} className="m-2 ml-4" />
</InputContainer>
</SettingsContainer>
);

View file

@ -1,4 +1,4 @@
import { Button, Input } from '@sd/ui';
import { Button, Card, GridLayout, Input } from '@sd/ui';
import { MagnifyingGlass } from 'phosphor-react';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
@ -45,13 +45,14 @@ function ExtensionItem(props: { extension: ExtensionItemData }) {
const { installed, name, description } = props.extension;
return (
<div className="flex flex-col w-[290px] px-4 py-4 bg-gray-600 border border-gray-500 rounded">
<h3 className="m-0 text-sm font-bold">{name}</h3>
<p className="mt-1 mb-1 text-xs text-gray-300 ">{description}</p>
<Button size="sm" className="mt-2" variant={installed ? 'gray' : 'primary'}>
<Card className="flex-col">
<h3 className="mt-2 text-sm font-bold">{name}</h3>
<p className="mt-1 mb-1 text-xs text-gray-300">{description}</p>
<div className="flex-grow" />
<Button size="sm" className="my-2" variant={installed ? 'gray' : 'accent'}>
{installed ? 'Installed' : 'Install'}
</Button>
</div>
</Card>
);
}
@ -71,11 +72,11 @@ export default function ExtensionSettings() {
}
/>
<div className="flex flex-wrap gap-3">
<GridLayout>
{extensions.map((extension) => (
<ExtensionItem key={extension.uuid} extension={extension} />
))}
</div>
</GridLayout>
</SettingsContainer>
);
}

View file

@ -1,16 +1,18 @@
import { useBridgeQuery, usePlatform } from '@sd/client';
import { Input } from '@sd/ui';
import { getDebugState, useBridgeQuery, useDebugState, usePlatform } from '@sd/client';
import { Card, Input, Switch, tw } from '@sd/ui';
import { Database } from 'phosphor-react';
import Card from '../../../components/layout/Card';
import { Toggle } from '../../../components/primitive';
import { InputContainer } from '../../../components/primitive/InputContainer';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
export default function GeneralSettings() {
const { data: node } = useBridgeQuery(['getNode']);
const NodePill = tw.div`px-1.5 py-[2px] rounded text-xs font-medium bg-app-selected`;
const NodeSettingLabel = tw.div`mb-1 text-xs font-medium`;
export default function GeneralSettings() {
const { data: node } = useBridgeQuery(['nodeState']);
const platform = usePlatform();
const debugState = useDebugState();
return (
<SettingsContainer>
@ -18,60 +20,58 @@ export default function GeneralSettings() {
title="General Settings"
description="General settings related to this client."
/>
<Card className="px-5 dark:bg-gray-600">
<Card className="px-5">
<div className="flex flex-col w-full my-2">
<div className="flex">
<div className="flex flex-row items-center justify-between">
<span className="font-semibold">Connected Node</span>
<div className="flex-grow" />
<div className="space-x-2">
<span className="px-2 py-[2px] rounded text-xs font-medium bg-gray-500">0 Peers</span>
<span className="px-1.5 py-[2px] rounded text-xs font-medium bg-primary-600">
Running
</span>
<div className="flex flex-row space-x-1">
<NodePill>0 Peers</NodePill>
<NodePill className="text-white !bg-accent">Running</NodePill>
</div>
</div>
<hr className="mt-2 mb-4 border-gray-500 " />
<div className="flex flex-row space-x-4">
<hr className="mt-2 mb-4 border-app-line" />
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-col">
<span className="mb-1 text-xs font-medium text-gray-700 dark:text-gray-100">
Node Name
</span>
<NodeSettingLabel>Node Name</NodeSettingLabel>
<Input value={node?.name} />
</div>
<div className="flex flex-col w-[100px]">
<span className="mb-1 text-xs font-medium text-gray-700 dark:text-gray-100">
Node Port
</span>
<div className="flex flex-col">
<NodeSettingLabel>Node Port</NodeSettingLabel>
<Input contentEditable={false} value={node?.p2p_port || 5795} />
</div>
<div className="flex flex-col w-[295px]">
<span className="mb-1 text-xs font-medium text-gray-700 dark:text-gray-100">
Node ID
</span>
<Input contentEditable={false} value={node?.id} />
</div>
</div>
<div className="flex items-center mt-5 space-x-3">
<Toggle size="sm" value />
<span className="text-sm text-gray-200">Run daemon when app closed</span>
<Switch size="sm" checked />
<span className="text-sm font-medium text-ink-dull">Run daemon when app closed</span>
</div>
<div className="mt-3">
<span
<div
onClick={() => {
if (node && platform?.openLink) {
platform.openLink(node.data_path);
}
}}
className="text-xs font-medium text-gray-700 dark:text-gray-400"
className="text-sm font-medium text-ink-faint"
>
<Database className="inline w-4 h-4 mr-2 -mt-[2px]" />
<b className="mr-2">Data Folder</b>
<b className="inline mr-2 truncate">
<Database className="inline w-4 h-4 mr-1 -mt-[2px]" /> Data Folder
</b>
<span className="select-text">{node?.data_path}</span>
</span>
</div>
</div>
</div>
</Card>
<InputContainer
mini
title="Debug mode"
description="Enable extra debugging features within the app. Enabling this could have unintended consequences so be warned!"
>
<Switch
checked={debugState.enabled}
onClick={() => (getDebugState().enabled = !debugState.enabled)}
/>
</InputContainer>
</SettingsContainer>
);
}

View file

@ -1,6 +1,6 @@
import { Switch } from '@sd/ui';
import { useState } from 'react';
import { Toggle } from '../../../components/primitive';
import { InputContainer } from '../../../components/primitive/InputContainer';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
@ -9,13 +9,18 @@ export default function AppearanceSettings() {
const [syncWithLibrary, setSyncWithLibrary] = useState(true);
return (
<SettingsContainer>
<SettingsHeader title="Keybindings" description="Manage client keybindings" />
{/* I don't care what you think the "right" way to write "keybinds" is, I simply refuse to refer to it as "keybindings" */}
<SettingsHeader title="Keybinds" description="Manage client keybinds" />
<InputContainer
mini
title="Sync with Library"
description="If enabled your keybindings will be synced with library, otherwise they will apply only to this client."
description="If enabled your keybinds will be synced with library, otherwise they will apply only to this client."
>
<Toggle value={syncWithLibrary} onChange={setSyncWithLibrary} className="m-2 ml-4" />
<Switch
checked={syncWithLibrary}
onCheckedChange={setSyncWithLibrary}
className="m-2 ml-4"
/>
</InputContainer>
</SettingsContainer>
);

View file

@ -1,10 +1,30 @@
import { Switch } from '@sd/ui';
import { useState } from 'react';
import { InputContainer } from '../../../components/primitive/InputContainer';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
export default function PrivacySettings() {
const [uiAnimations, setUiAnimations] = useState(true);
const [blurEffects, setBlurEffects] = useState(true);
return (
<SettingsContainer>
<SettingsHeader title="Privacy" description="How Spacedrive handles your data" />
<SettingsHeader title="Permissions" description="" />
<InputContainer
mini
title="UI Animations"
description="Dialogs and other UI elements will animate when opening and closing."
>
<Switch checked={uiAnimations} onCheckedChange={setUiAnimations} className="m-2 ml-4" />
</InputContainer>
<InputContainer
mini
title="Blur Effects"
description="Some components will have a blur effect applied to them."
>
<Switch checked={blurEffects} onCheckedChange={setBlurEffects} className="m-2 ml-4" />
</InputContainer>
</SettingsContainer>
);
}

View file

@ -1,10 +1,18 @@
import { useBridgeQuery } from '@sd/client';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
export default function AboutSpacedrive() {
const buildInfo = useBridgeQuery(['buildInfo']);
return (
<SettingsContainer>
<SettingsHeader title="About Spacedrive" description="The file manager from the future." />
<h1 className="!m-0 text-sm">
Build: v{buildInfo.data?.version || '-.-.-'} - {buildInfo.data?.commit || 'dev'}
</h1>
</SettingsContainer>
);
}

View file

@ -1,36 +1,26 @@
import { useBridgeMutation } from '@sd/client';
import { useCurrentLibrary } from '@sd/client';
import { Button, Input } from '@sd/ui';
import { useEffect, useState } from 'react';
import { Button, Input, Switch } from '@sd/ui';
import { useForm } from 'react-hook-form';
import { useDebouncedCallback } from 'use-debounce';
import { Toggle } from '../../../components/primitive';
import { InputContainer } from '../../../components/primitive/InputContainer';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
import { useDebouncedForm } from '../../../hooks/useDebouncedForm';
export default function LibraryGeneralSettings() {
const { library } = useCurrentLibrary();
const { mutate: editLibrary } = useBridgeMutation('library.edit');
const debounced = useDebouncedCallback((value) => {
const form = useForm({
defaultValues: { id: library!.uuid, ...library?.config }
});
useDebouncedForm(form, (value) =>
editLibrary({
id: library!.uuid,
name: value.name,
description: value.description
});
}, 500);
const { register, watch, getValues } = useForm({
defaultValues: {
name: library?.config.name,
description: library?.config.description
}
});
watch(debounced); // Listen for form changes
// This forces the debounce to run when the component is unmounted
useEffect(() => () => debounced.flush(), [debounced]);
})
);
return (
<SettingsContainer>
@ -40,14 +30,12 @@ export default function LibraryGeneralSettings() {
/>
<div className="flex flex-row pb-3 space-x-5">
<div className="flex flex-col flex-grow">
<span className="mb-1 text-sm font-medium text-gray-700 dark:text-gray-100">Name</span>
<Input {...register('name', { required: true })} defaultValue="My Default Library" />
<span className="mb-1 text-sm font-medium">Name</span>
<Input {...form.register('name', { required: true })} defaultValue="My Default Library" />
</div>
<div className="flex flex-col flex-grow">
<span className="mb-1 text-sm font-medium text-gray-700 dark:text-gray-100">
Description
</span>
<Input {...register('description')} placeholder="" />
<span className="mb-1 text-sm font-medium">Description</span>
<Input {...form.register('description')} placeholder="" />
</div>
</div>
@ -57,7 +45,7 @@ export default function LibraryGeneralSettings() {
description="Enable encryption for this library, this will only encrypt the Spacedrive database, not the files themselves."
>
<div className="flex items-center ml-3">
<Toggle value={false} />
<Switch checked={false} />
</div>
</InputContainer>
<InputContainer mini title="Export Library" description="Export this library to a file.">

View file

@ -1,6 +1,7 @@
import { useLibraryMutation, useLibraryQuery, usePlatform } from '@sd/client';
import { LocationCreateArgs } from '@sd/client';
import { Button } from '@sd/ui';
import { Button, Input } from '@sd/ui';
import { MagnifyingGlass } from 'phosphor-react';
import LocationListItem from '../../../components/location/LocationListItem';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
@ -18,9 +19,13 @@ export default function LocationSettings() {
title="Locations"
description="Manage your storage locations."
rightArea={
<div className="flex-row space-x-2">
<div className="flex flex-row items-center space-x-5">
<div className="relative hidden lg:block">
<MagnifyingGlass className="absolute w-[18px] h-auto top-[8px] left-[11px] text-gray-350" />
<Input className="!p-0.5 !pl-9" placeholder="Search locations" />
</div>
<Button
variant="primary"
variant="accent"
size="sm"
onClick={() => {
if (!platform.openFilePickerDialog) {

View file

@ -1,15 +1,12 @@
import { TrashIcon } from '@heroicons/react/24/outline';
import { Tag, useLibraryMutation, useLibraryQuery } from '@sd/client';
import { TagUpdateArgs } from '@sd/client';
import { Button, Input } from '@sd/ui';
import { Dialog } from '@sd/ui';
import { Button, Card, Dialog, Input, Switch } from '@sd/ui';
import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useDebounce } from 'rooks';
import Card from '../../../components/layout/Card';
import { Toggle } from '../../../components/primitive';
import { InputContainer } from '../../../components/primitive/InputContainer';
import { PopoverPicker } from '../../../components/primitive/PopoverPicker';
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
@ -17,6 +14,7 @@ import { SettingsHeader } from '../../../components/settings/SettingsHeader';
export default function TagsSettings() {
const [openCreateModal, setOpenCreateModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
// creating new tag state
const [newColor, setNewColor] = useState('#A717D9');
const [newName, setNewName] = useState('');
@ -55,7 +53,6 @@ export default function TagsSettings() {
);
const submitTagUpdate = handleSubmit((data) => updateTag.mutate(data));
// eslint-disable-next-line react-hooks/exhaustive-deps
const autoUpdateTag = useCallback(useDebounce(submitTagUpdate, 500), []);
@ -73,7 +70,7 @@ export default function TagsSettings() {
<div className="flex-row space-x-2">
<Dialog
open={openCreateModal}
onOpenChange={setOpenCreateModal}
setOpen={setOpenCreateModal}
title="Create New Tag"
description="Choose a name and color."
ctaAction={() => {
@ -85,7 +82,7 @@ export default function TagsSettings() {
loading={isLoading}
ctaLabel="Create"
trigger={
<Button variant="primary" size="sm">
<Button variant="accent" size="sm">
Create Tag
</Button>
}
@ -107,7 +104,7 @@ export default function TagsSettings() {
</div>
}
/>
<Card className="!px-2 dark:bg-gray-800">
<Card className="!px-2">
<div className="flex flex-wrap gap-2 m-1">
{tags?.map((tag) => (
<div
@ -154,6 +151,8 @@ export default function TagsSettings() {
</div>
<div className="flex flex-grow" />
<Dialog
open={openDeleteModal}
setOpen={setOpenDeleteModal}
title="Delete Tag"
description="Are you sure you want to delete this tag? This cannot be undone and tagged files will be unlinked."
ctaAction={() => {
@ -174,7 +173,7 @@ export default function TagsSettings() {
title="Show in Spaces"
description="Show this tag on the spaces screen."
>
<Toggle value />
<Switch checked />
</InputContainer>
</form>
) : (

Some files were not shown because too many files have changed in this diff Show more