mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-08 07:12:49 +00:00
Merge pull request #431 from spacedriveapp/spacedrive-but-themable
New color system & better UI components
This commit is contained in:
commit
4493f15065
|
@ -14,6 +14,7 @@ Flac
|
|||
haden
|
||||
haoyuan
|
||||
haris
|
||||
Iconoir
|
||||
josephjacks
|
||||
justinhoffman
|
||||
Keychain
|
||||
|
|
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
|
@ -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
46
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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!())?;
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
"title": "Spacedrive",
|
||||
"width": 1400,
|
||||
"height": 725,
|
||||
"minWidth": 700,
|
||||
"minWidth": 768,
|
||||
"minHeight": 500,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -110,7 +110,7 @@ function Page() {
|
|||
<Button
|
||||
onClick={scrollToPositions}
|
||||
className="z-30 border-0 cursor-pointer"
|
||||
variant="primary"
|
||||
variant="accent"
|
||||
>
|
||||
See Open Positions
|
||||
</Button>
|
||||
|
|
|
@ -3,7 +3,6 @@ import config from './docs';
|
|||
|
||||
export async function onBeforeRender() {
|
||||
const navigation = getDocsNavigation(config);
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
pageProps: {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
8715
apps/mobile/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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} />
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 } |
|
||||
|
|
|
@ -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]
|
||||
})
|
||||
]
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="dark">
|
||||
<html class="vanilla-theme">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Spacedrive</title>
|
||||
|
|
|
@ -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
11
core/build.rs
Normal 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);
|
||||
}
|
|
@ -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;
|
|
@ -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?) })
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
73
crates/sync/docs/HLC.md
Normal 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());
|
||||
};
|
||||
```
|
|
@ -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
|
|
@ -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 -- ",
|
||||
|
|
4
packages/assets/svgs/caret.svg
Normal file
4
packages/assets/svgs/caret.svg
Normal 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 |
3
packages/assets/svgs/ellipsis.svg
Normal file
3
packages/assets/svgs/ellipsis.svg
Normal 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 |
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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();
|
||||
}, []);
|
||||
|
||||
|
|
23
packages/client/src/stores/debugState.ts
Normal file
23
packages/client/src/stores/debugState.ts
Normal 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;
|
||||
}
|
|
@ -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>
|
||||
};
|
||||
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export * from './explorerStore';
|
||||
export * from './debugState';
|
||||
export * from './util';
|
||||
|
|
32
packages/client/src/stores/themeStore.ts
Normal file
32
packages/client/src/stores/themeStore.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:*",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
|
@ -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]}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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]'
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" />;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
)}
|
||||
|
|
|
@ -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">•</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">•</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, ',');
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}`
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
75
packages/interface/src/components/primitive/Toasts.tsx
Normal file
75
packages/interface/src/components/primitive/Toasts.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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']
|
||||
}
|
||||
})
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from './Toggle';
|
|
@ -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} />
|
||||
|
|
27
packages/interface/src/hooks/useDebouncedForm.ts
Normal file
27
packages/interface/src/hooks/useDebouncedForm.ts
Normal 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]);
|
||||
}
|
0
packages/interface/src/hooks/useMediaQuery.ts
Normal file
0
packages/interface/src/hooks/useMediaQuery.ts
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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.">
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue