Merge remote-tracking branch 'origin/main' into eng-1793-introduce-error-handling-for-sd-core-sync-crate

This commit is contained in:
Vítor Vasconcellos 2024-07-03 17:22:16 -03:00
commit 077bf01a40
No known key found for this signature in database
GPG key ID: 6EF4F6B456957E38
77 changed files with 1845 additions and 890 deletions

309
Cargo.lock generated
View file

@ -99,12 +99,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "aligned-vec"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
@ -132,6 +126,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_log-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@ -196,23 +196,6 @@ version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
[[package]]
name = "arg_enum_proc_macro"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "arrayref"
version = "0.3.7"
@ -520,29 +503,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "av1-grain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
dependencies = [
"anyhow",
"arrayvec",
"log",
"nom",
"num-rational",
"v_frame",
]
[[package]]
name = "avif-serialize"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2"
dependencies = [
"arrayvec",
]
[[package]]
name = "aws-config"
version = "1.5.1"
@ -1157,12 +1117,6 @@ dependencies = [
"serde",
]
[[package]]
name = "bitstream-io"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c12d1856e42f0d817a835fe55853957c85c8c8a470114029143d3f12671446e"
[[package]]
name = "bitvec"
version = "1.0.1"
@ -1311,12 +1265,6 @@ dependencies = [
"serde",
]
[[package]]
name = "built"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6a6c0b39c38fd754ac338b00a88066436389c0f029da5d37d1e01091d9b7c17"
[[package]]
name = "builtin-psl-connectors"
version = "0.1.0"
@ -1356,12 +1304,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.6.0"
@ -4431,35 +4373,20 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.1"
version = "0.24.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"exr",
"gif",
"image-webp",
"jpeg-decoder",
"num-traits",
"png",
"qoi",
"ravif",
"rayon",
"rgb",
"tiff",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d730b085583c4d789dfd07fdcf185be59501666a90c97c40162b37e4fdad272d"
dependencies = [
"byteorder-lite",
"thiserror",
]
[[package]]
@ -4468,12 +4395,6 @@ version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
[[package]]
name = "imgref"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
[[package]]
name = "include_dir"
version = "0.7.3"
@ -4589,17 +4510,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "interpolate_name"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "ipconfig"
version = "0.3.2"
@ -4643,15 +4553,6 @@ version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]]
name = "iter_tools"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f85582248e8796b1d7146eabe9f70c5b9de4db16bf934ca893581d33c66403b6"
dependencies = [
"itertools 0.11.0",
]
[[package]]
name = "itertools"
version = "0.10.5"
@ -4661,15 +4562,6 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.12.1"
@ -4759,6 +4651,9 @@ name = "jpeg-decoder"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
dependencies = [
"rayon",
]
[[package]]
name = "js-sys"
@ -4962,17 +4857,6 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "libfuzzer-sys"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
dependencies = [
"arbitrary",
"cc",
"once_cell",
]
[[package]]
name = "libheif-rs"
version = "1.0.2"
@ -5593,15 +5477,6 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "loop9"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
dependencies = [
"imgref",
]
[[package]]
name = "lru"
version = "0.7.8"
@ -5739,16 +5614,6 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
[[package]]
name = "maybe-rayon"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
"cfg-if",
"rayon",
]
[[package]]
name = "md-5"
version = "0.10.6"
@ -6309,12 +6174,6 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "noop_proc_macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "normpath"
version = "1.2.0"
@ -6420,17 +6279,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "num-integer"
version = "0.1.46"
@ -7002,8 +6850,9 @@ dependencies = [
[[package]]
name = "pdfium-render"
version = "0.8.21"
source = "git+https://github.com/fogodev/pdfium-render.git?rev=e7aa1111f441c49e857cebda15b4e51b24356aaa#e7aa1111f441c49e857cebda15b4e51b24356aaa"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d2f2669618a1ae68a86fe975fdda1c6a0b6dc9d5358ea8d2cdcd8da3307e5a"
dependencies = [
"bindgen",
"bitflags 2.5.0",
@ -7013,7 +6862,7 @@ dependencies = [
"console_error_panic_hook",
"console_log",
"image",
"iter_tools",
"itertools 0.13.0",
"js-sys",
"libloading 0.8.3",
"log",
@ -7677,19 +7526,6 @@ name = "profiling"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
dependencies = [
"profiling-procmacros",
]
[[package]]
name = "profiling-procmacros"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
dependencies = [
"quote",
"syn 2.0.66",
]
[[package]]
name = "prometheus-client"
@ -7887,12 +7723,6 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-protobuf"
version = "0.8.1"
@ -8077,56 +7907,6 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rav1e"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
dependencies = [
"arbitrary",
"arg_enum_proc_macro",
"arrayvec",
"av1-grain",
"bitstream-io",
"built",
"cfg-if",
"interpolate_name",
"itertools 0.12.1",
"libc",
"libfuzzer-sys",
"log",
"maybe-rayon",
"new_debug_unreachable",
"noop_proc_macro",
"num-derive",
"num-traits",
"once_cell",
"paste",
"profiling",
"rand 0.8.5",
"rand_chacha 0.3.1",
"simd_helpers",
"system-deps",
"thiserror",
"v_frame",
"wasm-bindgen",
]
[[package]]
name = "ravif"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc13288f5ab39e6d7c9d501759712e6969fcc9734220846fc9ed26cae2cc4234"
dependencies = [
"avif-serialize",
"imgref",
"loop9",
"quick-error 2.0.1",
"rav1e",
"rayon",
"rgb",
]
[[package]]
name = "raw-cpuid"
version = "10.7.0"
@ -8397,7 +8177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
dependencies = [
"hostname 0.3.1",
"quick-error 1.2.3",
"quick-error",
]
[[package]]
@ -8941,7 +8721,7 @@ dependencies = [
[[package]]
name = "sd-core"
version = "0.3.1"
version = "0.3.2"
dependencies = [
"async-channel",
"async-recursion",
@ -9027,6 +8807,7 @@ dependencies = [
"tokio-util",
"tower-service",
"tracing",
"tracing-android",
"tracing-appender",
"tracing-subscriber",
"tracing-test",
@ -9174,7 +8955,7 @@ dependencies = [
[[package]]
name = "sd-desktop"
version = "0.3.1"
version = "0.3.2"
dependencies = [
"axum",
"dbus",
@ -9866,15 +9647,6 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simd_helpers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
dependencies = [
"quote",
]
[[package]]
name = "simplecss"
version = "0.2.1"
@ -11250,6 +11022,17 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-android"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12612be8f868a09c0ceae7113ff26afe79d81a24473a393cb9120ece162e86c0"
dependencies = [
"android_log-sys",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "tracing-appender"
version = "0.2.3"
@ -11816,17 +11599,6 @@ dependencies = [
"serde",
]
[[package]]
name = "v_frame"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b"
dependencies = [
"aligned-vec",
"num-traits",
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.0"
@ -12097,9 +11869,9 @@ dependencies = [
[[package]]
name = "webp"
version = "0.3.0"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f53152f51fb5af0c08484c33d16cca96175881d1f3dec068c23b31a158c2d99"
checksum = "4bb5d8e7814e92297b0e1c773ce43d290bef6c17452dafd9fc49e5edb5beba71"
dependencies = [
"image",
"libwebp-sys",
@ -13125,12 +12897,6 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-inflate"
version = "0.2.54"
@ -13140,15 +12906,6 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "zune-jpeg"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448"
dependencies = [
"zune-core",
]
[[package]]
name = "zvariant"
version = "4.0.0"

View file

@ -36,7 +36,7 @@ gix-ignore = "0.11.2"
globset = "0.4.14"
http = "0.2" # Update blocked by axum
hyper = "0.14" # Update blocked due to API breaking changes
image = "0.25.1"
image = "0.24.9" # Update blocked due to https://github.com/image-rs/image/issues/2230
itertools = "0.13.0"
lending-stream = "1.0"
libc = "0.2"
@ -66,7 +66,7 @@ tracing-subscriber = "0.3.18"
tracing-test = "0.2.5"
uhlc = "0.6.0" # Must follow version used by specta
uuid = "1.8"
webp = "0.3.0"
webp = "0.2.6" # Update blocked by image
[workspace.dependencies.prisma-client-rust]
git = "https://github.com/brendonovich/prisma-client-rust"
@ -101,8 +101,6 @@ libp2p-stream = { git = "https://github.com/spacedriveapp/rust-libp2p.git", rev
blake3 = { git = "https://github.com/spacedriveapp/blake3.git", rev = "d3aab416c12a75c2bfabce33bcd594e428a79069" }
# Due to image crate version bump
pdfium-render = { git = "https://github.com/fogodev/pdfium-render.git", rev = "e7aa1111f441c49e857cebda15b4e51b24356aaa" }
[profile.dev]
# Make compilation faster on macOS
@ -143,7 +141,7 @@ incremental = false
# Optimize release builds
[profile.release]
panic = "abort" # Strip expensive panic clean-up logic
panic = "unwind" # Sadly we need unwind to avoid unexpected crashes on third party crates
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size

View file

@ -1,6 +1,6 @@
[package]
name = "sd-desktop"
version = "0.3.1"
version = "0.3.2"
description = "The universal file manager."
authors = ["Spacedrive Technology Inc <support@spacedrive.com>"]
default-run = "sd-desktop"

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -4,8 +4,8 @@ import Link from 'next/link';
export interface NewBannerProps {
headline: string;
href: string;
link: string;
href?: string;
link?: string;
className?: string;
}
@ -14,7 +14,7 @@ export function NewBanner(props: NewBannerProps) {
return (
<Link
href={href}
href={href ?? '/'}
className={clsx(
props.className,
'news-banner-border-gradient news-banner-glow animation-delay-1 fade-in-whats-new z-10 mb-5 flex w-fit flex-row rounded-full bg-black/10 px-5 py-2.5 text-xs backdrop-blur-md transition hover:bg-purple-900/20 sm:w-auto sm:text-base'
@ -24,10 +24,14 @@ export function NewBanner(props: NewBannerProps) {
<Newspaper weight="fill" className="text-white " size={20} />
<p className="font-regular truncate text-white">{headline}</p>
</div>
<div role="separator" className="h-22 mx-4 w-px bg-zinc-700/70" />
<span className="font-regular shrink-0 bg-gradient-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-transparent decoration-primary-600">
{link} <span aria-hidden="true">&rarr;</span>
</span>
{link && (
<>
<div role="separator" className="h-22 mx-4 w-px bg-zinc-700/70" />
<span className="font-regular shrink-0 bg-gradient-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-transparent decoration-primary-600">
{link} <span aria-hidden="true">&rarr;</span>
</span>
</>
)}
</Link>
);
}

View file

@ -1,8 +1,8 @@
import { z } from 'zod';
import { object, z } from 'zod';
import { env } from '~/env';
import * as github from './github';
import { createSlashCommand, createViewSubmission, USER_REF } from './utils';
import { createBlockActions, createSlashCommand, createViewSubmission, USER_REF } from './utils';
export const callbackId = 'createReleaseModal' as const;
export const fields = {
@ -17,7 +17,7 @@ export const fields = {
} as const;
export const COMMAND_NAME = '/release' as const;
export const EVENT_SCHEMAS = [createSlashCommand(COMMAND_NAME), createViewSubmission()] as const;
export const EVENT_SCHEMAS = [createSlashCommand(COMMAND_NAME), createViewSubmission(), createBlockActions()] as const;
export async function createModal(
trigger_id: string,
@ -63,7 +63,7 @@ export async function createModal(
type: 'plain_text',
text: 'View Commit'
},
url: `${github.REPO_API}/commit/${commit}`
url: `${github.REPO_API}/commits/${commit}`
},
text: {
type: 'mrkdwn',
@ -185,7 +185,18 @@ export async function handleSubmission(
const { tag, commit, responseUrl } = JSON.parse(privateMetadata);
await fetch(`${github.REPO_API}/git/refs`, {
const createTag = await fetch(`${github.REPO_API}/git/tags`, {
method: 'POST',
body: JSON.stringify({
tag,
message: tagline,
object: commit,
type: 'commit'
}),
headers: github.HEADERS
}).then((r) => r.json());
const getRef = await fetch(`${github.REPO_API}/git/refs`, {
method: 'POST',
body: JSON.stringify({
ref: `refs/tags/${tag}`,
@ -222,6 +233,7 @@ export async function handleSubmission(
headers: github.HEADERS
}
);
const [release] = await Promise.all([createRelease, dispatchWorkflowRun]);
await fetch(responseUrl, {
@ -267,7 +279,7 @@ export async function handleSubmission(
type: 'plain_text',
text: 'View Commit'
},
url: `https://github.com/${env.GITHUB_ORG}/${env.GITHUB_REPO}/commit/${commit}`
url: `https://github.com/${env.GITHUB_ORG}/${env.GITHUB_REPO}/commits/${commit}`
}
]
}

View file

@ -16,6 +16,7 @@ export async function POST(req: Request) {
if (!isValid.valid) return new Response(isValid.error, { status: 400 });
const parsedBody = BODY.safeParse(Object.fromEntries([...new URLSearchParams(body)]));
if (!parsedBody.success) {
console.log(parsedBody.error);
return new Response('Unexpected request', { status: 400 });

View file

@ -42,11 +42,91 @@ export function createViewSubmission() {
export function createSlashCommand<T extends string>(command: T) {
return z.object({
command: z.literal(command),
token: z.string(),
team_id: z.string(),
team_domain: z.string(),
channel_id: z.string(),
text: z.string().transform((s) => s.split(' ')),
channel_name: z.string(),
user_id: z.string(),
trigger_id: z.string(),
response_url: z.string()
user_name: z.string(),
command: z.literal(command),
text: z.string().transform((s) => s.split(' ')),
api_app_id: z.string(),
is_enterprise_install: z.union([z.literal('false'), z.literal('true')]).transform((v) => v === 'true'),
response_url: z.string(),
trigger_id: z.string()
});
}
const BLOCK_ACTIONS_INNER = z.object({
api_app_id: z.string(),
token: z.string(),
container: z.object({
type: z.string(),
message_ts: z.string(),
channel_id: z.string(),
is_ephemeral: z.boolean()
}),
trigger_id: z.string(),
team: z.object({
id: z.string(),
domain: z.string()
}),
enterprise: z.any().nullable(),
is_enterprise_install: z.boolean(),
channel: z.object({
id: z.string(),
name: z.string()
}),
message: z.object({
subtype: z.string(),
text: z.string(),
type: z.string(),
ts: z.string(),
bot_id: z.string(),
blocks: z.array(
z.object({
type: z.string(),
block_id: z.string(),
text: z.object({
type: z.string(),
text: z.string(),
verbatim: z.boolean()
}).optional(),
elements: z.array(
z.object({
type: z.string(),
action_id: z.string(),
text: z.object({
type: z.string(),
text: z.string(),
emoji: z.boolean()
}),
url: z.string().optional()
})
).optional()
})
)
}),
state: z.object({
values: z.record(z.record(z.any()))
}),
response_url: z.string(),
actions: z.array(
z.object({
action_id: z.string(),
block_id: z.string(),
text: z.object({
type: z.string(),
text: z.string(),
emoji: z.boolean()
}),
type: z.string(),
action_ts: z.string()
})
)
});
export function createBlockActions() {
return createInteraction('block_actions', BLOCK_ACTIONS_INNER);
}

View file

@ -39,12 +39,7 @@ export default async function Page() {
<div className="flex w-full flex-col items-center px-4">
<div className="mt-22 lg:mt-28" id="content" aria-hidden="true" />
<div className="mt-24 lg:mt-8" />
<NewBanner
headline="Alpha 0.2 is here!"
href="/blog/alpha-zero-two-release"
link="Read post"
className="mt-[50px] lg:mt-0"
/>
<NewBanner headline="Alpha 0.3 is here!" className="mt-[50px] lg:mt-0" />
<h1 className="fade-in-heading z-30 mb-3 bg-clip-text px-2 text-center text-4xl font-bold leading-tight text-white md:text-5xl lg:text-7xl">
One Explorer. All Your Files.
</h1>

View file

@ -13,24 +13,50 @@ export const teamMembers: Array<TeamMemberProps> = [
}
},
{
name: 'Utku Bakır',
location: 'Toronto, Canada',
role: 'Product Manager & TypeScript Engineer',
imageUrl: '/images/team/utku.jpg',
socials: {
github: 'https://github.com/utkubakir'
}
name: 'Valerie Wong',
location: 'Vancouver, Canada',
role: 'Director of Operations',
imageUrl: '/images/team/valerie.jpeg',
},
{
name: 'Ericson Soares',
location: 'Rio de Janeiro, Brazil',
role: 'Rust Engineer - Core & VDFS',
role: 'Head of Engineering - Rust',
imageUrl: '/images/team/ericson.jpg',
socials: {
twitter: 'https://x.com/fogodev',
github: 'https://github.com/fogodev'
}
},
{
name: 'Ameer Al Ashhab',
location: 'Amman, Jordan',
role: 'Product Manager & TypeScript Engineer - Interface Design',
imageUrl: '/images/team/ameer.jpg',
socials: {
github: 'https://github.com/ameer2468'
}
},
{
name: 'Matheus Consoli',
location: 'São Paulo, Brazil',
role: 'Rust Engineer',
imageUrl: '/images/team/matheus.jpg',
socials: {
github: 'https://github.com/matheus-consoli',
}
},
{
name: 'Vítor Vasconcellos',
location: 'Rio de Janeiro, Brazil',
role: 'Fullstack Devops Engineer',
imageUrl: '/images/team/vitor.jpg',
socials: {
github: 'https://github.com/HeavenVolkoff',
gitlab: 'https://gitlab.com/VitorVasconcellos',
twitter: 'https://x.com/vasvas10'
}
},
{
name: 'Mihail Dounaev',
location: 'Finland',
@ -41,26 +67,6 @@ export const teamMembers: Array<TeamMemberProps> = [
dribbble: 'https://dribbble.com/mmmint'
}
},
{
name: 'Ameer Al Ashhab',
location: 'Jordan',
role: 'TypeScript Engineer - Interface Design',
imageUrl: '/images/team/ameer.jpg',
socials: {
github: 'https://github.com/ameer2468'
}
},
{
name: 'Vítor Vasconcellos',
location: 'Rio de Janeiro, Brazil',
role: 'TypeScript Engineer',
imageUrl: '/images/team/vitor.jpg',
socials: {
github: 'https://github.com/HeavenVolkoff',
gitlab: 'https://gitlab.com/VitorVasconcellos',
twitter: 'https://x.com/vasvas10'
}
},
{
name: 'Matthew Yung',
location: 'Vancouver, Canada',

View file

@ -1,18 +1,18 @@
import { useDrawerStatus } from '@react-navigation/drawer';
import { useNavigation } from '@react-navigation/native';
import { useClientContext } from '@sd/client';
import { MotiView } from 'moti';
import { CaretRight, CloudArrowDown, Gear, Lock, Plus } from 'phosphor-react-native';
import { useEffect, useRef, useState } from 'react';
import { Alert, Pressable, Text, View } from 'react-native';
import { useClientContext } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import { currentLibraryStore } from '~/utils/nav';
import { AnimatedHeight } from '../animation/layout';
import { ModalRef } from '../layout/Modal';
import CreateLibraryModal from '../modal/CreateLibraryModal';
import { Divider } from '../primitive/Divider';
import ImportModalLibrary from '../modal/ImportLibraryModal';
import { Divider } from '../primitive/Divider';
const DrawerLibraryManager = () => {
const [dropdownClosed, setDropdownClosed] = useState(true);
@ -24,7 +24,6 @@ const DrawerLibraryManager = () => {
}, [isDrawerOpen]);
const { library: currentLibrary, libraries } = useClientContext();
const navigation = useNavigation();
const modalRef = useRef<ModalRef>(null);
@ -58,7 +57,6 @@ const DrawerLibraryManager = () => {
>
{/* Libraries */}
{libraries.data?.map((library) => {
// console.log('library', library);
return (
<Pressable
key={library.uuid}

View file

@ -45,7 +45,7 @@ const DrawerLocationItem: React.FC<DrawerLocationItemProps> = ({
)}
/>
</View>
<Text style={twStyle('text-xs font-medium text-ink')} numberOfLines={1}>
<Text style={twStyle('max-w-[150px] text-xs font-medium text-ink')} numberOfLines={1}>
{location.name ?? ''}
</Text>
</View>

View file

@ -1,16 +1,15 @@
import { DocumentDirectoryPath } from '@dr.pogodin/react-native-fs';
import { getIcon } from '@sd/assets/util';
import {
ThumbKey,
getExplorerItemData,
getItemLocation,
isDarkTheme,
type ExplorerItem
} from '@sd/client';
import { Image } from 'expo-image';
import { useEffect, useLayoutEffect, useMemo, useState, type PropsWithChildren } from 'react';
import { View } from 'react-native';
import {
getExplorerItemData,
getItemFilePath,
getItemLocation,
isDarkTheme,
ThumbKey,
type ExplorerItem
} from '@sd/client';
import { flattenThumbnailKey, useExplorerStore } from '~/stores/explorerStore';
import { tw } from '../../lib/tailwind';
@ -71,7 +70,6 @@ type FileThumbProps = {
export default function FileThumb({ size = 1, ...props }: FileThumbProps) {
const itemData = useExplorerItemData(props.data);
const locationData = getItemLocation(props.data);
const filePath = getItemFilePath(props.data);
const [src, setSrc] = useState<null | string>(null);
const [thumbType, setThumbType] = useState(ThumbType.Icon);
@ -132,7 +130,6 @@ export default function FileThumb({ size = 1, ...props }: FileThumbProps) {
break;
}
}, [itemData, thumbType]);
return (
<FileThumbWrapper size={size}>
{(() => {

View file

@ -1,8 +1,8 @@
import { useNavigation } from '@react-navigation/native';
import { useLibraryQuery } from '@sd/client';
import { DotsThree } from 'phosphor-react-native';
import React from 'react';
import { Text, View } from 'react-native';
import { uint32ArrayToBigInt, useLibraryQuery } from '@sd/client';
import { tw } from '~/lib/tailwind';
import { OverviewStackScreenProps } from '~/navigation/tabs/OverviewStack';
@ -24,12 +24,17 @@ export default function CategoriesScreen() {
style={tw`h-8 w-8 rounded-full`}
variant="gray"
>
<DotsThree weight='bold' size={18} color={'white'} />
<DotsThree weight="bold" size={18} color={'white'} />
</Button>
</View>
<View style={tw`flex-row flex-wrap gap-2`}>
{kinds.data?.statistics
?.sort((a, b) => b.count - a.count)
?.sort((a, b) => {
const aCount = Number(a.count);
const bCount = Number(b.count);
if (aCount === bCount) return 0;
return aCount > bCount ? -1 : 1;
})
.filter((i) => i.kind !== 0)
.slice(0, 6)
.map((item) => {
@ -49,7 +54,7 @@ export default function CategoriesScreen() {
kind={kind}
name={name}
icon={icon}
items={count}
items={Number(count)}
/>
);
})}

View file

@ -1,16 +1,16 @@
import { formatNumber } from '@sd/client';
import { useNavigation } from '@react-navigation/native';
import { Pressable, Text, View } from 'react-native';
import { ClassInput } from 'twrnc';
import { formatNumber } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import { useNavigation } from '@react-navigation/native';
import { useSearchStore } from '~/stores/searchStore';
import { Icon, IconName } from '../icons/Icon';
interface CategoryItemProps {
kind: number;
name: string;
items: number;
items: bigint | number;
icon: IconName;
selected?: boolean;
onClick?: () => void;
@ -29,14 +29,18 @@ const CategoryItem = ({ name, icon, items, style, kind }: CategoryItemProps) =>
style
)}
onPress={() => {
searchStore.updateFilters('kind', {
name,
icon: icon + '20' as IconName,
id: kind
}, true);
searchStore.updateFilters(
'kind',
{
name,
icon: (icon + '20') as IconName,
id: kind
},
true
);
navigation.navigate('SearchStack', {
screen: 'Search',
})
screen: 'Search'
});
}}
>
<Icon name={icon} size={56} />

View file

@ -1,12 +1,11 @@
import { IconTypes } from '@sd/assets/util';
import { ObjectKind } from '@sd/client';
import { MotiView } from 'moti';
import { memo, useCallback, useMemo } from 'react';
import { FlatList, Pressable, Text, View } from 'react-native';
import { LinearTransition } from 'react-native-reanimated';
import { ObjectKind } from '@sd/client';
import { Icon } from '~/components/icons/Icon';
import Card from '~/components/layout/Card';
import Fade from '~/components/layout/Fade';
import SectionTitle from '~/components/layout/SectionTitle';
import VirtualizedListWrapper from '~/components/layout/VirtualizedListWrapper';
import { tw, twStyle } from '~/lib/tailwind';
@ -40,12 +39,11 @@ const Kind = () => {
sub="What kind of objects should be searched?"
/>
<View>
<Fade color="black" width={30} height="100%">
<VirtualizedListWrapper horizontal>
<FlatList
data={kinds}
renderItem={({ item }) => <KindFilter data={item} />}
contentContainerStyle={tw`pl-6`}
contentContainerStyle={tw`px-6`}
numColumns={kinds && Math.ceil(Number(kinds.length) / 2)}
key={kinds ? 'kindsSearch' : '_'}
scrollEnabled={false}
@ -56,7 +54,6 @@ const Kind = () => {
style={tw`flex-row`}
/>
</VirtualizedListWrapper>
</Fade>
</View>
</MotiView>
);

View file

@ -38,7 +38,7 @@ export default function TagScreen({ navigation, route }: BrowseStackScreenProps<
includeHeaderHeight
icon={'Tags'}
style={tw`flex-1 items-center justify-center border-0`}
iconSize={100}
iconSize={80}
description={'No items assigned to this tag'}
/>} {...objects} />;
}

View file

@ -1,7 +1,7 @@
import { useLibraryQuery } from '@sd/client';
import { useMemo } from 'react';
import { FlatList, View } from 'react-native';
import { useDebounce } from 'use-debounce';
import { uint32ArrayToBigInt, useLibraryQuery } from '@sd/client';
import { IconName } from '~/components/icons/Icon';
import ScreenContainer from '~/components/layout/ScreenContainer';
import CategoryItem from '~/components/overview/CategoryItem';
@ -22,7 +22,14 @@ const CategoriesScreen = () => {
return (
<ScreenContainer scrollview={false} style={tw`relative px-6 py-0`}>
<FlatList
data={filteredKinds?.sort((a, b) => b.count - a.count).filter((i) => i.kind !== 0)}
data={filteredKinds
?.sort((a, b) => {
const aCount = Number(a.count);
const bCount = Number(b.count);
if (aCount === bCount) return 0;
return aCount > bCount ? -1 : 1;
})
.filter((i) => i.kind !== 0)}
numColumns={3}
contentContainerStyle={tw`py-6`}
keyExtractor={(item) => item.name}
@ -46,7 +53,7 @@ const CategoriesScreen = () => {
kind={kind}
name={name}
icon={icon}
items={count}
items={Number(count)}
/>
);
}}

View file

@ -153,7 +153,7 @@ export default function SettingsScreen({ navigation }: SettingsStackScreenProps<
const debugState = useDebugState();
return (
<ScreenContainer tabHeight={false} scrollview={false} style={tw`gap-0 px-6 py-0`}>
<ScreenContainer tabHeight={false} style={tw`gap-0 px-5 py-0`}>
<SectionList
contentContainerStyle={tw`py-6`}
sections={sections(debugState)}
@ -165,6 +165,7 @@ export default function SettingsScreen({ navigation }: SettingsStackScreenProps<
rounded={item.rounded}
/>
)}
scrollEnabled={false}
renderSectionHeader={renderSectionHeader}
ListFooterComponent={<FooterComponent />}
showsVerticalScrollIndicator={false}

View file

@ -1,9 +1,8 @@
import { LibraryConfigWrapped, useBridgeQuery, useLibraryContext } from '@sd/client';
import { DotsThreeOutlineVertical, Pen, Trash } from 'phosphor-react-native';
import React, { useEffect, useRef } from 'react';
import { Animated, FlatList, Pressable, Text, View } from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
import { LibraryConfigWrapped, useBridgeQuery } from '@sd/client';
import Fade from '~/components/layout/Fade';
import { ModalRef } from '~/components/layout/Modal';
import ScreenContainer from '~/components/layout/ScreenContainer';
import DeleteLibraryModal from '~/components/modal/confirmModals/DeleteLibraryModal';
@ -14,10 +13,12 @@ import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
function LibraryItem({
library,
index,
navigation
navigation,
current
}: {
library: LibraryConfigWrapped;
index: number;
current: boolean;
navigation: SettingsStackScreenProps<'LibrarySettings'>['navigation'];
}) {
const renderRightActions = (
@ -63,8 +64,15 @@ function LibraryItem({
>
<View style={tw`flex-row items-center justify-between`}>
<View>
<View style={tw`flex-row items-center gap-2`}>
<Text style={tw`text-md font-semibold text-ink`}>{library.config.name}</Text>
<Text style={tw`mt-1 text-xs text-ink-dull`}>{library.uuid}</Text>
{current && (
<View style={tw`rounded-md bg-accent px-1.5 py-[2px]`}>
<Text style={tw`text-xs font-semibold text-white`}>Current</Text>
</View>
)}
</View>
<Text style={tw`mt-1.5 text-xs text-ink-dull`}>{library.uuid}</Text>
</View>
<Pressable onPress={() => swipeRef.current?.openRight()}>
<DotsThreeOutlineVertical
@ -81,6 +89,7 @@ function LibraryItem({
const LibrarySettingsScreen = ({ navigation }: SettingsStackScreenProps<'LibrarySettings'>) => {
const libraryList = useBridgeQuery(['library.list']);
const libraries = libraryList.data;
const { library } = useLibraryContext();
useEffect(() => {
navigation.setOptions({
@ -101,22 +110,19 @@ const LibrarySettingsScreen = ({ navigation }: SettingsStackScreenProps<'Library
return (
<ScreenContainer style={tw`justify-start gap-0 px-6 py-0`} scrollview={false}>
<Fade
fadeSides="top-bottom"
orientation="vertical"
color="black"
width={30}
height="100%"
>
<FlatList
data={libraries}
contentContainerStyle={tw`py-5`}
keyExtractor={(item) => item.uuid}
renderItem={({ item, index }) => (
<LibraryItem navigation={navigation} library={item} index={index} />
<LibraryItem
current={item.uuid === library.uuid}
navigation={navigation}
library={item}
index={index}
/>
)}
/>
</Fade>
</ScreenContainer>
);
};

View file

@ -1,8 +1,8 @@
import { useBridgeQuery } from '@sd/client';
import { Image } from 'expo-image';
import { Globe } from 'phosphor-react-native';
import React from 'react';
import { Linking, Platform, Text, View } from 'react-native';
import { useBridgeQuery } from '@sd/client';
import { DiscordIcon, GitHubIcon } from '~/components/icons/Brands';
import ScreenContainer from '~/components/layout/ScreenContainer';
import { Button } from '~/components/primitive/Button';
@ -98,7 +98,7 @@ const AboutScreen = () => {
<Image
source={{ uri: 'https://i.imgur.com/SwUcWHP.png' }}
style={{ height: 200, width: '100%' }}
resizeMode="contain"
contentFit='contain'
/>
</View>
</ScreenContainer>

View file

@ -41,7 +41,7 @@ const Library = ({ cloudLibrary }: LibraryProps) => {
</InfoBox>
<Button
disabled={syncLibrary.isLoading || thisInstance !== undefined}
variant={thisInstance ? 'gray' : 'accent'}
variant="gray"
onPress={() => syncLibrary.mutate(null)}
style={tw`mt-2 flex-row gap-1 py-2`}
>

View file

@ -1,4 +1,5 @@
import { Text, View } from 'react-native';
import { Icon } from '~/components/icons/Icon';
import Card from '~/components/layout/Card';
import { Button } from '~/components/primitive/Button';
import { tw } from '~/lib/tailwind';
@ -12,14 +13,17 @@ const Login = () => {
};
return (
<View style={tw`flex-1 flex-col items-center justify-center gap-2`}>
<Card style={tw`w-full items-center justify-center p-6`}>
<Card style={tw`w-full items-center justify-center py-6`}>
<View style={tw`flex-col items-center gap-2`}>
<Icon name="CloudSync" size={64} />
<Text style={tw`mb-4 max-w-[60%] text-center text-ink`}>
To access cloud related features, please login
</Text>
</View>
{(authState.status === 'notLoggedIn' || authState.status === 'loggingIn') && (
<Button
variant="accent"
style={tw`mx-auto max-w-[50%]`}
style={tw`mx-auto mt-1 max-w-[50%]`}
onPress={async (e) => {
e.preventDefault();
if (authState.status === 'loggingIn') {

View file

@ -1,14 +1,15 @@
import { inferSubscriptionResult } from '@oscartbeaumont-sd/rspc-client';
import { MotiView } from 'moti';
import { Circle } from 'phosphor-react-native';
import React, { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import {
Procedures,
useLibraryMutation,
useLibraryQuery,
useLibrarySubscription
} from '@sd/client';
import { MotiView } from 'moti';
import { Circle } from 'phosphor-react-native';
import React, { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import { Icon } from '~/components/icons/Icon';
import Card from '~/components/layout/Card';
import ScreenContainer from '~/components/layout/ScreenContainer';
import { Button } from '~/components/primitive/Button';
@ -35,14 +36,20 @@ const SyncSettingsScreen = ({ navigation }: SettingsStackScreenProps<'SyncSettin
<ScreenContainer scrollview={false} style={tw`gap-0 px-6`}>
{syncEnabled.data === false ? (
<View style={tw`flex-1 justify-center`}>
<Card style={tw`relative py-10`}>
<Card style={tw`relative flex-col items-center gap-5 py-6`}>
<View style={tw`flex-col items-center gap-2`}>
<Icon name="Sync" size={64} />
<Text style={tw`max-w-[70%] text-center leading-5 text-ink`}>
To enable sync, please start the backfill operations
</Text>
</View>
<Button
variant={'accent'}
style={tw`mx-auto max-w-[82%]`}
onPress={() => setStart(true)}
>
<Text style={tw`font-medium text-white`}>
Start Backfill Operations
Start backfill
</Text>
</Button>
</Card>

View file

@ -1,6 +1,6 @@
[package]
name = "sd-core"
version = "0.3.1"
version = "0.3.2"
description = "Virtual distributed filesystem engine that powers Spacedrive."
authors = ["Spacedrive Technology Inc <support@spacedrive.com>"]
rust-version = "1.78"
@ -159,6 +159,9 @@ icrate = { version = "0.1.2", features = [
"Foundation_NSNumber",
] }
[target.'cfg(target_os = "android")'.dependencies]
tracing-android = "0.2.0"
[dev-dependencies]
# Workspace dependencies
globset = { workspace = true }

View file

@ -30,6 +30,9 @@ pub enum JobSystemError {
#[error(transparent)]
Report(#[from] ReportError),
#[error("internal job panic! <id='{0}'>")]
Panic(JobId),
}
impl From<JobSystemError> for rspc::Error {

View file

@ -13,6 +13,7 @@ use std::{
hash::{Hash, Hasher},
marker::PhantomData,
ops::{Deref, DerefMut},
panic::AssertUnwindSafe,
path::Path,
pin::pin,
sync::Arc,
@ -21,7 +22,7 @@ use std::{
use async_channel as chan;
use chrono::{DateTime, Utc};
use futures::{stream, Future, StreamExt};
use futures::{stream, Future, FutureExt, StreamExt};
use futures_concurrency::{
future::{Join, TryJoin},
stream::Merge,
@ -750,15 +751,29 @@ where
trace!("Dispatching job");
spawn(to_spawn_job::<OuterCtx, _, _>(
self.id,
self.job,
ctx.clone(),
None,
base_dispatcher,
commands_rx,
done_tx,
));
spawn({
let id = self.id;
let job = self.job;
let ctx = ctx.clone();
async move {
if AssertUnwindSafe(to_spawn_job::<OuterCtx, _, _>(
id,
job,
ctx,
None,
base_dispatcher,
commands_rx,
done_tx,
))
.catch_unwind()
.await
.is_err()
{
error!("job panicked");
}
}
});
JobHandle {
id: self.id,
@ -791,15 +806,29 @@ where
trace!("Resuming job");
spawn(to_spawn_job::<OuterCtx, _, _>(
self.id,
self.job,
ctx.clone(),
serialized_tasks,
base_dispatcher,
commands_rx,
done_tx,
));
spawn({
let id = self.id;
let job = self.job;
let ctx = ctx.clone();
async move {
if AssertUnwindSafe(to_spawn_job::<OuterCtx, _, _>(
id,
job,
ctx,
serialized_tasks,
base_dispatcher,
commands_rx,
done_tx,
))
.catch_unwind()
.await
.is_err()
{
error!("job panicked");
}
}
});
JobHandle {
id: self.id,
@ -855,9 +884,14 @@ async fn to_spawn_job<OuterCtx, JobCtx, J>(
spawn(
async move {
tx.send(job.run::<OuterCtx>(dispatcher, ctx).await)
.await
.expect("job run channel closed");
tx.send(
AssertUnwindSafe(job.run::<OuterCtx>(dispatcher, ctx))
.catch_unwind()
.await
.unwrap_or(Err(Error::JobSystem(JobSystemError::Panic(job_id)))),
)
.await
.expect("job run channel closed");
}
.in_current_span(),
);

View file

@ -14,6 +14,7 @@ use sd_file_ext::extensions::{VideoExtension, ALL_VIDEO_EXTENSIONS};
use std::{
ops::Deref,
panic,
path::{Path, PathBuf},
str::FromStr,
time::Duration,
@ -313,9 +314,9 @@ pub async fn generate_thumbnail(
}
fn inner_generate_image_thumbnail(
file_path: PathBuf,
file_path: &PathBuf,
) -> Result<Vec<u8>, thumbnailer::NonCriticalThumbnailerError> {
let mut img = format_image(&file_path).map_err(|e| {
let mut img = format_image(file_path).map_err(|e| {
thumbnailer::NonCriticalThumbnailerError::FormatImage(file_path.clone(), e.to_string())
})?;
@ -336,7 +337,7 @@ fn inner_generate_image_thumbnail(
// this corrects the rotation/flip of the image based on the *available* exif data
// not all images have exif data, so we don't error. we also don't rotate HEIF as that's against the spec
if let Some(orientation) = Orientation::from_path(&file_path) {
if let Some(orientation) = Orientation::from_path(file_path) {
if ConvertibleExtension::try_from(file_path.as_ref())
.expect("we already checked if the image was convertible")
.should_rotate()
@ -347,7 +348,10 @@ fn inner_generate_image_thumbnail(
// Create the WebP encoder for the above image
let encoder = Encoder::from_image(&img).map_err(|reason| {
thumbnailer::NonCriticalThumbnailerError::WebPEncoding(file_path, reason.to_string())
thumbnailer::NonCriticalThumbnailerError::WebPEncoding(
file_path.clone(),
reason.to_string(),
)
})?;
// Type `WebPMemory` is !Send, which makes the `Future` in this function `!Send`,
@ -378,7 +382,19 @@ async fn generate_image_thumbnail(
move || {
// Handling error on receiver side
let _ = tx.send(inner_generate_image_thumbnail(file_path));
let _ = tx.send(
panic::catch_unwind(|| inner_generate_image_thumbnail(&file_path)).unwrap_or_else(
move |_| {
Err(
thumbnailer::NonCriticalThumbnailerError::PanicWhileGeneratingThumbnail(
file_path,
"Internal panic on third party crate".to_string(),
),
)
},
),
);
}
});

View file

@ -6,17 +6,15 @@ use crate::{
Node,
};
use futures::StreamExt;
use prisma_client_rust::raw;
use sd_core_heavy_lifting::JobId;
use sd_file_ext::kind::ObjectKind;
use sd_p2p::RemoteIdentity;
use sd_prisma::prisma::{indexer_rule, object, statistics};
use tokio_stream::wrappers::IntervalStream;
use tracing::{info, warn};
use sd_prisma::prisma::{file_path, indexer_rule, object, statistics};
use sd_utils::{db::size_in_bytes_from_db, u64_to_frontend};
use std::{
collections::{hash_map::Entry, HashMap},
collections::{hash_map::Entry, BTreeMap, HashMap},
convert::identity,
pin::pin,
sync::Arc,
@ -25,8 +23,13 @@ use std::{
use async_channel as chan;
use directories::UserDirs;
use futures_concurrency::{future::Join, stream::Merge};
use futures::StreamExt;
use futures_concurrency::{
future::{Join, TryJoin},
stream::Merge,
};
use once_cell::sync::Lazy;
use prisma_client_rust::{and, or, raw};
use rspc::{alpha::AlphaRouter, ErrorCode};
use serde::{Deserialize, Serialize};
use specta::Type;
@ -36,7 +39,8 @@ use tokio::{
sync::Mutex,
time::{interval, Instant},
};
use tracing::{debug, error};
use tokio_stream::wrappers::IntervalStream;
use tracing::{debug, error, info, warn};
use uuid::Uuid;
use super::{utils::library, Ctx, R};
@ -122,36 +126,125 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
})
})
.procedure("kindStatistics", {
#[derive(Serialize, Deserialize, Type, Default)]
#[derive(Debug, Serialize, Deserialize, Type, Default)]
pub struct KindStatistic {
kind: i32,
name: String,
count: i32,
total_bytes: String,
count: (u32, u32),
total_bytes: (u32, u32),
}
#[derive(Serialize, Deserialize, Type, Default)]
#[derive(Debug, Serialize, Deserialize, Type, Default)]
pub struct KindStatistics {
statistics: Vec<KindStatistic>,
total_identified_files: i32,
total_unidentified_files: i32,
}
#[derive(Default)]
struct CountAndSize {
count: u64,
size: u64,
}
R.with2(library()).query(|(_, library), _: ()| async move {
let mut statistics: Vec<KindStatistic> = vec![];
for kind in ObjectKind::iter() {
let count = library
let (total_unidentified_files, total_identified_files) = (
library
.db
.file_path()
.count(vec![
file_path::is_dir::equals(Some(false)),
file_path::cas_id::equals(None),
file_path::object_id::equals(None),
])
.exec(),
library
.db
.file_path()
.count(vec![or!(
file_path::is_dir::equals(Some(true)),
and!(
file_path::cas_id::not(None),
file_path::object_id::not(None),
),
)])
.exec(),
)
.try_join()
.await?;
let mut statistics_by_kind = BTreeMap::from_iter(
ObjectKind::iter().map(|kind| (kind as i32, CountAndSize::default())),
);
let mut last_object_id = 0;
loop {
let objects = library
.db
.object()
.count(vec![object::kind::equals(Some(kind as i32))])
.find_many(vec![object::id::gt(last_object_id)])
.take(1000)
.select(
object::select!({ id kind file_paths: select { size_in_bytes_bytes } }),
)
.exec()
.await?;
statistics.push(KindStatistic {
kind: kind as i32,
name: kind.to_string(),
count: count as i32,
total_bytes: "0".to_string(),
});
if let Some(last) = objects.last() {
last_object_id = last.id;
} else {
break; // No more objects
}
for object in objects {
if let Some(kind) = object.kind {
statistics_by_kind.entry(kind).and_modify(|count_and_size| {
count_and_size.count += object.file_paths.len() as u64;
count_and_size.size += object
.file_paths
.into_iter()
.map(|file_path| {
file_path
.size_in_bytes_bytes
.map(|size| size_in_bytes_from_db(&size))
.unwrap_or(0)
})
.sum::<u64>();
});
}
}
}
Ok(KindStatistics { statistics })
// This is a workaround for the fact that we don't assign object to directories yet
if let Some(count_and_size) =
statistics_by_kind.get_mut(&(ObjectKind::Folder as i32))
{
count_and_size.count = library
.db
.file_path()
.count(vec![file_path::is_dir::equals(Some(true))])
.exec()
.await? as u64;
}
Ok(KindStatistics {
statistics: ObjectKind::iter()
.map(|kind| {
let int_kind = kind as i32;
let CountAndSize { count, size } =
statistics_by_kind.get(&int_kind).expect("can't fail");
KindStatistic {
kind: int_kind,
name: kind.to_string(),
count: u64_to_frontend(*count),
total_bytes: u64_to_frontend(*size),
}
})
.collect(),
total_identified_files: total_identified_files as i32,
total_unidentified_files: total_unidentified_files as i32,
})
})
})
.procedure("create", {

View file

@ -31,7 +31,7 @@ use tracing_appender::{
non_blocking::{NonBlocking, WorkerGuard},
rolling::{RollingFileAppender, Rotation},
};
use tracing_subscriber::{filter::FromEnvError, prelude::*, EnvFilter};
use tracing_subscriber::{filter::FromEnvError, prelude::*, registry, EnvFilter};
pub mod api;
mod cloud;
@ -254,12 +254,15 @@ impl Node {
);
}
tracing_subscriber::registry()
let registry = registry();
let registry = registry
.with(
tracing_subscriber::fmt::layer()
.with_file(true)
.with_line_number(true)
.with_ansi(false)
.with_target(true)
.with_writer(logfile)
.with_filter(EnvFilter::from_default_env()),
)
@ -269,8 +272,12 @@ impl Node {
.with_line_number(true)
.with_writer(std::io::stdout)
.with_filter(EnvFilter::from_default_env()),
)
.init();
);
#[cfg(target_os = "android")]
let registry = registry.with(tracing_android::layer("com.spacedrive.app").unwrap());
registry.init();
std::panic::set_hook(Box::new(move |panic| {
use std::backtrace::{Backtrace, BacktraceStatus};

View file

@ -1,11 +1,12 @@
use crate::{api::utils::get_size, library::Library, volume::get_volumes, Node};
use sd_prisma::prisma::statistics;
use sd_utils::db::size_in_bytes_from_db;
use chrono::Utc;
use tracing::{error, info};
use super::LibraryManagerError;
use tracing::{error, info};
pub async fn update_library_statistics(
node: &Node,
@ -45,7 +46,7 @@ pub async fn update_library_statistics(
.map(|location| {
location
.size_in_bytes
.map(|bytes| bytes.iter().fold(0, |acc, &x| acc * 256 + x as u64))
.map(|size| size_in_bytes_from_db(&size))
.unwrap_or(0)
})
.sum::<u64>();

View file

@ -44,7 +44,7 @@ impl<E: RunError> System<E> {
let workers_count = usize::max(
std::thread::available_parallelism().map_or_else(
|e| {
error!("Failed to get available parallelism in the job system: {e:#?}");
error!(?e, "Failed to get available parallelism in the job system");
1
},
NonZeroUsize::get,

View file

@ -1,6 +1,8 @@
use std::{
any::Any,
collections::{HashMap, VecDeque},
future::pending,
panic::AssertUnwindSafe,
pin::pin,
sync::{
atomic::{AtomicBool, Ordering},
@ -53,7 +55,7 @@ struct AbortAndSuspendSignalers {
struct RunningTask {
id: TaskId,
kind: PendingTaskKind,
handle: JoinHandle<()>,
handle: JoinHandle<Result<(), Box<dyn Any + Send>>>,
}
enum WaitingSuspendedTask {
@ -150,7 +152,7 @@ impl<E: RunError> Runner<E> {
&mut self,
task_id: TaskId,
task_work_state: TaskWorkState<E>,
) -> JoinHandle<()> {
) -> JoinHandle<Result<(), Box<dyn Any + Send>>> {
let (abort_tx, abort_rx) = oneshot::channel();
let (suspend_tx, suspend_rx) = oneshot::channel();
@ -163,13 +165,16 @@ impl<E: RunError> Runner<E> {
);
let handle = spawn(
run_single_task(
task_work_state,
self.task_output_tx.clone(),
suspend_rx,
abort_rx,
AssertUnwindSafe(
run_single_task(
task_work_state,
self.task_output_tx.clone(),
suspend_rx,
abort_rx,
)
.in_current_span(),
)
.in_current_span(),
.catch_unwind(),
);
trace!("Task runner spawned");
@ -624,8 +629,14 @@ impl<E: RunError> Runner<E> {
}
}
if let Err(e) = handle.await {
error!(%task_id, ?e, "Task failed to join");
match handle.await {
Ok(Ok(())) => { /* Everything is Awesome! */ }
Ok(Err(_)) => {
error!(%task_id, "Task panicked");
}
Err(e) => {
error!(%task_id, ?e, "Task failed to join");
}
}
stolen_task_tx.close();
@ -806,8 +817,14 @@ impl<E: RunError> Runner<E> {
assert_eq!(*finished_task_id, old_task_id, "Task output id mismatch"); // Sanity check
if let Err(e) = handle.await {
error!(?e, "Task failed to join");
match handle.await {
Ok(Ok(())) => { /* Everything is Awesome! */ }
Ok(Err(_)) => {
error!("Task panicked");
}
Err(e) => {
error!(?e, "Task failed to join");
}
}
if let Some((next_task_kind, task_work_state)) = self.get_next_task() {

View file

@ -1,8 +1,4 @@
import { AppWindow, ArrowSquareOut, CaretRight, ClipboardText } from '@phosphor-icons/react';
import clsx from 'clsx';
import { memo, useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import { createSearchParams } from 'react-router-dom';
import {
getExplorerItemData,
getIndexedItemFilePath,
@ -10,9 +6,13 @@ import {
useLibraryQuery
} from '@sd/client';
import { ContextMenu } from '@sd/ui';
import clsx from 'clsx';
import { memo, useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import { createSearchParams } from 'react-router-dom';
import { useTabsContext } from '~/TabsContext';
import { Icon } from '~/components';
import { useIsDark, useLocale, useOperatingSystem } from '~/hooks';
import { useTabsContext } from '~/TabsContext';
import { usePlatform } from '~/util/Platform';
import { useExplorerContext } from './Context';
@ -119,10 +119,10 @@ export const ExplorerPathBar = memo(() => {
return (
<div
className={clsx(
'group flex items-center border-t border-t-app-line bg-app/90 px-3.5 text-[11px] text-ink-dull backdrop-blur-lg',
`h-[${PATH_BAR_HEIGHT}px]`
)}
className={'group flex items-center border-t border-t-app-line bg-app/90 px-3.5 text-[11px] text-ink-dull backdrop-blur-lg'}
style={{
height: PATH_BAR_HEIGHT
}}
>
{paths.map((path, idx) => (
<Path

View file

@ -1,8 +1,9 @@
import { Circle } from '@phosphor-icons/react';
import clsx from 'clsx';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import {
ExplorerItem,
getItemObject,
Tag,
Target,
useLibraryMutation,
@ -17,7 +18,7 @@ import { keybindForOs } from '~/util/keybinds';
import { useExplorerContext } from './Context';
import { explorerStore } from './store';
export const TAG_BAR_HEIGHT = 64;
export const TAG_BAR_HEIGHT = 54;
const NUMBER_KEYCODES: string[][] = [
['Key1'],
['Key2'],
@ -87,27 +88,30 @@ export const ExplorerTagBar = () => {
const element = tagsRef.current;
if (element) {
setIsTagsOverflowing(
element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth
element.scrollHeight > element.clientHeight ||
element.scrollWidth > element.clientWidth
);
}
}
};
useEffect(() => {
const element = tagsRef.current;
if (!element) return;
//handles initial render when not resizing
setIsTagsOverflowing(element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth)
setIsTagsOverflowing(
element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth
);
//make sure state updates when window resizing
window.addEventListener('resize', () => {
updateOverflowState();
})
});
//remove listeners on unmount
return () => {
window.removeEventListener('resize', () => {
updateOverflowState();
})
}
}, [tagsRef])
});
};
}, [tagsRef]);
const [tagListeningForKeyPress, setTagListeningForKeyPress] = useState<number | undefined>();
@ -147,22 +151,51 @@ export const ExplorerTagBar = () => {
if (!tag) return;
try {
await mutation.mutateAsync({
targets,
tag_id: tag.id,
unassign: false
});
// extract the list of tags from each object in the selected items
const targetsTagList = Array.from(explorer.selectedItems.entries()).map(
// issues with type here. unsure as to why, and not causing any noticeable errors, so ignoring for now with as any
(item) => (item[0] as any).item.object.tags
);
toast(
t('tags_bulk_assigned', {
tag_name: tag.name,
file_count: targets.length
}),
{
type: 'success'
}
);
// iterate through each tag in the selected items and check if the tag we want to assign is already assigned
const areAllAssigned = targetsTagList.every((tags) => {
return tags.some((t: { tag_id: any }) => t.tag_id === tag.id);
});
try {
if (areAllAssigned) {
await mutation.mutateAsync({
targets,
tag_id: tag.id,
unassign: true
});
toast(
t('tags_bulk_unassigned', {
tag_name: tag.name,
file_count: targets.length
}),
{
type: 'success'
}
);
} else {
await mutation.mutateAsync({
targets,
tag_id: tag.id,
unassign: false
});
toast(
t('tags_bulk_assigned', {
tag_name: tag.name,
file_count: targets.length
}),
{
type: 'success'
}
);
}
} catch (err) {
let msg: string = t('error_unknown');
@ -199,7 +232,7 @@ export const ExplorerTagBar = () => {
return (
<div
className={clsx(
'flex flex-row flex-wrap-reverse items-center justify-between gap-1 border-t border-t-app-line bg-app/90 px-3.5 py-2 text-ink-dull backdrop-blur-lg',
'flex flex-row flex-wrap-reverse items-center justify-between gap-1 border-t border-t-app-line bg-app/90 px-3.5 py-2 text-ink-dull backdrop-blur-lg'
)}
>
<em className="text-sm tracking-wide">{t('tags_bulk_instructions')}</em>

View file

@ -12,6 +12,20 @@ import {
Icon as PhosphorIcon,
Snowflake
} from '@phosphor-icons/react';
import clsx from 'clsx';
import dayjs from 'dayjs';
import {
forwardRef,
useCallback,
useEffect,
useMemo,
useState,
type HTMLAttributes,
type ReactNode
} from 'react';
import { useLocation } from 'react-router';
import { Link as NavLink } from 'react-router-dom';
import Sticky from 'react-sticky-el';
import {
FilePath,
FilePathForFrontend,
@ -28,20 +42,6 @@ import {
type ExplorerItem
} from '@sd/client';
import { Button, Divider, DropdownMenu, toast, Tooltip, tw } from '@sd/ui';
import clsx from 'clsx';
import dayjs from 'dayjs';
import {
forwardRef,
useCallback,
useEffect,
useMemo,
useState,
type HTMLAttributes,
type ReactNode
} from 'react';
import { useLocation } from 'react-router';
import { Link as NavLink } from 'react-router-dom';
import Sticky from 'react-sticky-el';
import { LibraryIdParamsSchema } from '~/app/route-schemas';
import { Folder, Icon } from '~/components';
import { useLocale, useZodRouteParams } from '~/hooks';
@ -102,11 +102,7 @@ export const Inspector = forwardRef<HTMLDivElement, Props>(
const { t } = useLocale();
return (
<div ref={ref} style={{ width: INSPECTOR_WIDTH, ...style }} {...props}>
<Sticky
scrollElement={explorer.scrollRef.current || undefined}
stickyClassName="!top-[40px]"
topOffset={-40}
>
<Sticky stickyClassName="!top-[40px]" topOffset={-40}>
{showThumbnail && (
<div className="relative mb-2 flex aspect-square items-center justify-center px-2">
{isNonEmpty(selectedItems) ? (
@ -526,7 +522,6 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => {
const onlyNonIndexed = metadata.types.has('NonIndexedPath') && metadata.types.size === 1;
const filesSize = humanizeSize(metadata.size);
return (
<>
<MetaContainer>

View file

@ -432,14 +432,15 @@ export const QuickPreview = () => {
<div className="flex flex-1 items-center justify-end gap-1">
<Tooltip label={t('zoom_in')}>
<IconButton
onClick={() =>
setMagnification(
(currentMagnification) =>
currentMagnification +
currentMagnification * 0.2
)
}
// this is same formula as intrest calculation
onClick={() => {
magnification < 2 &&
setMagnification(
(currentMagnification) =>
currentMagnification +
currentMagnification * 0.2
);
}}
// this is same formula as interest calculation
>
<MagnifyingGlassPlus />
</IconButton>
@ -447,13 +448,14 @@ export const QuickPreview = () => {
<Tooltip label={t('zoom_out')}>
<IconButton
onClick={() =>
setMagnification(
(currentMagnification) =>
currentMagnification / (1 + 0.2)
)
}
// this is same formula as intrest calculation
onClick={() => {
magnification > 0.5 &&
setMagnification(
(currentMagnification) =>
currentMagnification / (1 + 0.2)
);
}}
// this is same formula as interest calculation
>
<MagnifyingGlassMinus />
</IconButton>

View file

@ -8,15 +8,15 @@ import {
SquaresFour,
Tag
} from '@phosphor-icons/react';
import { ExplorerLayout, useSelector } from '@sd/client';
import clsx from 'clsx';
import { useMemo } from 'react';
import { useDocumentEventListener } from 'rooks';
import { ExplorerLayout, useSelector } from '@sd/client';
import { useKeyMatcher, useLocale } from '~/hooks';
import { KeyManager } from '../KeyManager';
import { Spacedrop, SpacedropButton } from '../Spacedrop';
import TopBarOptions, { ToolOption, TOP_BAR_ICON_CLASSLIST } from '../TopBar/TopBarOptions';
import TopBarOptions, { TOP_BAR_ICON_CLASSLIST, ToolOption } from '../TopBar/TopBarOptions';
import { useExplorerContext } from './Context';
import OptionsPanel from './OptionsPanel';
import { explorerStore } from './store';
@ -83,7 +83,7 @@ export const useExplorerTopBarOptions = () => {
toolTipLabel: t('show_inspector'),
keybinds: [controlIcon, 'I'],
onClick: () => {
explorerStore.showInspector = !showInspector;
explorerStore.showInspector = !explorerStore.showInspector
},
icon: (
<SidebarSimple

View file

@ -1,5 +1,4 @@
import { FolderNotchOpen } from '@phosphor-icons/react';
import { CSSProperties, type PropsWithChildren, type ReactNode } from 'react';
import {
explorerLayout,
useExplorerLayoutStore,
@ -7,6 +6,7 @@ import {
useRspcLibraryContext,
useSelector
} from '@sd/client';
import { CSSProperties, useEffect, type PropsWithChildren, type ReactNode } from 'react';
import { useShortcut } from '~/hooks';
import { useTopBarContext } from '../TopBar/Context';
@ -26,7 +26,7 @@ import 'react-slidedown/lib/slidedown.css';
import clsx from 'clsx';
import { ExplorerTagBar } from './ExplorerTagBar';
import { ExplorerTagBar, TAG_BAR_HEIGHT } from './ExplorerTagBar';
import { useExplorerDnd } from './useExplorerDnd';
interface Props {
@ -92,11 +92,11 @@ export default function Explorer(props: PropsWithChildren<Props>) {
<ExplorerContextMenu>
<div
ref={explorer.scrollRef}
className="custom-scroll explorer-scroll flex flex-1 flex-col overflow-x-hidden"
className="explorer-scroll explorer-inspector-scroll flex flex-1 flex-col overflow-x-hidden"
style={
{
'--scrollbar-margin-top': `${topBar.topBarHeight}px`,
'--scrollbar-margin-bottom': `${showPathBar ? PATH_BAR_HEIGHT : 0}px`,
'--scrollbar-margin-bottom': `${showPathBar ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) : 0}px`,
'paddingTop': topBar.topBarHeight,
'paddingRight': showInspector ? INSPECTOR_WIDTH : 0
} as CSSProperties
@ -117,7 +117,7 @@ export default function Explorer(props: PropsWithChildren<Props>) {
listViewOptions={{ hideHeaderBorder: true }}
scrollPadding={{
top: topBar.topBarHeight,
bottom: showPathBar ? PATH_BAR_HEIGHT : undefined
bottom: showPathBar ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) : undefined
}}
/>
</div>
@ -132,11 +132,11 @@ export default function Explorer(props: PropsWithChildren<Props>) {
{showInspector && (
<Inspector
className={clsx(
'no-scrollbar absolute right-1.5 top-0 pb-3 pl-3 pr-1.5',
showPathBar && `b-[${PATH_BAR_HEIGHT}px]`
'no-scrollbar absolute right-1.5 top-0 pb-3 pl-3 pr-1.5'
)}
style={{
paddingTop: topBar.topBarHeight + 12
paddingTop: topBar.topBarHeight + 12,
bottom: showPathBar ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) : 0
}}
/>
)}

View file

@ -24,13 +24,13 @@ interface JobProps {
}
const JobIcon: Record<string, Icon> = {
indexer: Folder,
media_processor: Image,
file_identifier: Fingerprint,
file_copier: Copy,
file_deleter: Trash,
file_cutter: Scissors,
object_validator: Fingerprint
Indexer: Folder,
MediaProcessor: Image,
FileIdentifier: Fingerprint,
FileCopier: Copy,
FileDeleter: Trash,
FileCutter: Scissors,
ObjectValidator: Fingerprint
};
function Job({ job, className, isChild, progress }: JobProps) {

View file

@ -153,7 +153,7 @@ function usePlausible() {
useEffect(() => {
const interval = setInterval(() => {
plausibleEvent({ event: { type: 'ping' } });
}, 270 * 1000);
}, 600 * 1000); // 10 minutes
return () => clearInterval(interval);
}, [plausibleEvent]);

View file

@ -16,6 +16,9 @@ import { LoginButton } from '~/components/LoginButton';
import { useLocale, useRouteTitle } from '~/hooks';
import { hardwareModelToIcon } from '~/util/hardware';
const DataBox = tw.div`max-w-[300px] rounded-md border border-app-line/50 bg-app-lightBox/20 p-2`;
const Count = tw.div`min-w-[20px] flex h-[20px] px-1 items-center justify-center rounded-full border border-app-button/40 text-[9px]`;
export const Component = () => {
useRouteTitle('Cloud');
@ -26,10 +29,13 @@ export const Component = () => {
if (authState.status === 'notLoggedIn' || authState.status === 'loggingIn')
return (
<div className="flex size-full items-center justify-center">
<Card className="flex flex-col gap-4 !p-6">
<p>To access cloud related features, please login</p>
<DataBox className="flex flex-col items-center gap-5 !p-6">
<div className='flex flex-col items-center gap-1'>
<Icon name="Sync" size={60}/>
<p className='max-w-[75%] text-center text-sm'>To access cloud related features, please login</p>
</div>
<LoginButton />
</Card>
</DataBox>
</div>
);
@ -39,8 +45,6 @@ export const Component = () => {
return <div className="flex size-full flex-col items-start p-4">{authSensitiveChild()}</div>;
};
const DataBox = tw.div`max-w-[300px] rounded-md border border-app-line/50 bg-app-lightBox/20 p-2`;
const Count = tw.div`min-w-[20px] flex h-[20px] px-1 items-center justify-center rounded-full border border-app-button/40 text-[9px]`;
// million-ignore
function Authenticated() {
@ -73,7 +77,13 @@ function Authenticated() {
) : (
<div className="relative flex size-full flex-col items-center justify-center">
<AuthRequiredOverlay />
<DataBox className='flex min-w-[400px] flex-col items-center gap-5 p-6'>
<div className='flex flex-col items-center gap-2'>
<Icon name="CloudSync" size={60} />
<p className='max-w-[60%] text-center text-sm text-ink'>{t("cloud_connect_description")}</p>
</div>
<Button
className='h-8'
disabled={createLibrary.isLoading}
variant="accent"
onClick={() => {
@ -81,9 +91,13 @@ function Authenticated() {
}}
>
{createLibrary.isLoading
? t('connecting_library_to_cloud')
: t('connect_library_to_cloud')}
? <div className='flex h-4 flex-row items-center gap-2'>
<Loader className='w-5' color="white"/>
<p className='text-xs'>{t('Connecting' + '...')}</p>
</div>
: t('Connect')}
</Button>
</DataBox>
</div>
)}
</Suspense>

View file

@ -72,3 +72,263 @@ export const Component = () => {
</ExplorerContextProvider>
);
};
// NOTE -> this is code for the node graph. The plan is to implement this in network (moved from overview page). Jamie asked me to save the code somewhere
// so placing it here for now!
// import { getIcon } from '@sd/assets/util';
// import { useLibraryQuery } from '@sd/client';
// import React, { useEffect, useRef, useState, useCallback } from 'react';
// import { useIsDark } from '~/hooks';
// import ForceGraph2D from 'react-force-graph-2d';
// import { useNavigate } from 'react-router';
// import * as d3 from 'd3-force';
// //million-ignore
// const canvasWidth = 700
// const canvasHeight = 600;
// interface KindStatistic {
// kind: number;
// name: string;
// count: number;
// total_bytes: string;
// }
// interface Node {
// id: string | number;
// name: string;
// val: number;
// fx?: number;
// fy?: number;
// x?: number;
// y?: number;
// }
// interface Link {
// source: string | number;
// target: string | number;
// }
// interface GraphData {
// nodes: Node[];
// links: Link[];
// }
// const FileKindStatistics: React.FC = () => {
// const isDark = useIsDark();
// const navigate = useNavigate();
// const { data } = useLibraryQuery(['library.kindStatistics']);
// const [graphData, setGraphData] = useState<GraphData>({ nodes: [], links: [] });
// const iconsRef = useRef<{ [key: string]: HTMLImageElement }>({});
// const containerRef = useRef<HTMLDivElement>(null);
// const fgRef = useRef<any>(null);
// useEffect(() => {
// if (data) {
// const statistics: KindStatistic[] = data.statistics
// .filter((item: KindStatistic) => item.count != 0)
// .sort((a: KindStatistic, b: KindStatistic) => b.count - a.count)
// // TODO: eventually allow users to select and save which file kinds are shown
// .slice(0, 18); // Get the top 18 highest file kinds
// const totalFilesCount = statistics.reduce((sum, item) => sum + item.count, 0);
// const nodes = [
// { id: 'center', name: 'Total Files', val: totalFilesCount },
// ...statistics.map(item => ({
// id: item.kind,
// name: item.name,
// val: item.count,
// }))
// ];
// const links = statistics.map(item => ({
// source: 'center',
// target: item.kind,
// }));
// setGraphData({ nodes, links });
// // Preload icons, this is for rendering purposes
// statistics.forEach(item => {
// const iconName = item.name;
// if (!iconsRef.current[iconName]) {
// const img = new Image();
// img.src = getIcon(iconName, isDark);
// iconsRef.current[iconName] = img;
// }
// });
// // d3 stuff for changing physics of the nodes
// fgRef.current.d3Force('link').distance(110); // Adjust link distance to make links shorter
// fgRef.current.d3Force('charge').strength(-50); // how hard the nodes repel
// fgRef.current.d3Force('center').strength(10); // Adjust center strength for stability
// fgRef.current.d3Force('collision', d3.forceCollide().radius(25)); // Add collision force with radius. Should be a little larger than radius of nodes.
// fgRef.current.d3Force('y', d3.forceY(canvasHeight / 5).strength((1.2))); // strong force to ensure nodes don't spill out of canvas
// }
// }, [data, isDark]);
// const paintNode = useCallback((node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
// const fontSize = 0.6 / globalScale;
// ctx.font = `400 ${fontSize}em ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`;
// ctx.textAlign = 'center';
// ctx.textBaseline = 'middle';
// const darkColor = 'rgb(34, 34, 45)';
// const lightColor = 'rgb(252, 252, 254)';
// const x = isFinite(node.x) ? node.x : 0;
// const y = isFinite(node.y) ? node.y : 0;
// if (node.name === 'Total Files') {
// const radius = 25;
// const borderWidth = 0.5;
// // Create linear gradient for light mode
// const lightGradient = ctx.createLinearGradient(x - radius, y - radius, x + radius, y + radius);
// lightGradient.addColorStop(0, 'rgb(117, 177, 249)');
// lightGradient.addColorStop(1, 'rgb(0, 76, 153)');
// // Create linear gradient for dark mode
// const darkGradient = ctx.createLinearGradient(x - radius, y - radius, x + radius, y + radius);
// darkGradient.addColorStop(0, 'rgb(255, 13, 202)');
// darkGradient.addColorStop(1, 'rgb(128, 0, 255)');
// // Draw filled circle with gradient border
// ctx.beginPath();
// ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI, false);
// ctx.fillStyle = isDark ? darkGradient : lightGradient;
// ctx.fill();
// // Draw inner circle to create the border effect
// ctx.beginPath();
// ctx.arc(node.x, node.y, radius - borderWidth, 0, 2 * Math.PI, false);
// ctx.fillStyle = isDark ? darkColor : lightColor;
// ctx.fill();
// // Add inner shadow
// const shadowGradient = ctx.createRadialGradient(x, y, radius * 0.5, x, y, radius);
// shadowGradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
// shadowGradient.addColorStop(1, isDark ? 'rgba(255, 93, 234, 0.1' : 'rgba(66, 97, 255, 0.05)');
// ctx.globalCompositeOperation = 'source-atop';
// ctx.beginPath();
// ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI, false);
// ctx.fillStyle = shadowGradient;
// ctx.fill();
// // Draw text
// ctx.fillStyle = isDark ? 'rgba(255, 255, 255, 1)' : 'rgba(10, 10, 10, 0.8)';
// ctx.font = `bold ${fontSize * 2}em ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`;
// ctx.fillText(node.val, node.x, node.y - fontSize * 9);
// ctx.fillStyle = isDark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(10, 10, 10, 0.8)';
// ctx.font = `400 ${fontSize * 1.1}em ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`;
// ctx.fillText(node.name, node.x, node.y + fontSize * 25);
// } else {
// const iconName = node.name;
// const iconImg = iconsRef.current[iconName];
// const iconSize = 25 / globalScale;
// const textYPos = node.y + iconSize;
// // Draw shadow
// ctx.shadowColor = isDark ? 'rgb(44, 45, 58)' : 'rgba(0, 0, 0, 0.1)';
// ctx.shadowBlur = 0.5;
// ctx.shadowOffsetX = -0.5;
// ctx.shadowOffsetY = -2;
// // Draw node circle
// const radius = 18;
// ctx.beginPath();
// ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI, false);
// ctx.fillStyle = isDark ? darkColor : lightColor;
// ctx.fill();
// ctx.shadowColor = 'transparent';
// if (iconImg) {
// ctx.drawImage(iconImg, node.x - iconSize / 2, node.y - iconSize, iconSize, iconSize);
// }
// ctx.fillStyle = isDark ? 'white' : 'black';
// // Truncate node name if it is too long
// let truncatedName = node.name;
// if (node.name.length > 10) {
// truncatedName = node.name.slice(0, 6) + "...";
// }
// ctx.fillText(truncatedName, node.x, textYPos - 9);
// ctx.fillStyle = isDark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.5)';
// ctx.fillText(node.val, node.x, textYPos - 2);
// }
// }, [isDark]);
// const handleNodeClick = useCallback((node: any) => {
// if (node.id !== 'center') {
// const path = {
// pathname: '../search',
// search: new URLSearchParams({
// filters: JSON.stringify([{ object: { kind: { in: [node.id] } } }])
// }).toString()
// };
// navigate(path);
// }
// }, [navigate]);
// const handleEngineTick = () => {
// const centerNode = graphData.nodes.find((node: any) => node.id === 'center');
// if (centerNode) {
// centerNode.fx = 0;
// centerNode.fy = 0;
// }
// }
// useEffect(() => {
// if (fgRef.current) {
// fgRef.current.d3Force('center', d3.forceCenter());
// }
// }, []);
// const paintPointerArea = useCallback((node: any, color: string, ctx: CanvasRenderingContext2D, globalScale: number) => {
// const size = 30 / globalScale;
// ctx.fillStyle = color;
// ctx.beginPath();
// ctx.arc(node.x, node.y, size, 0, 2 * Math.PI, false);
// ctx.fill();
// }, []);
// return (
// <div className="relative bottom-48 h-[200px] w-full" ref={containerRef}>
// {data ? (
// <ForceGraph2D
// ref={fgRef}
// graphData={graphData}
// nodeId="id"
// linkSource="source"
// linkTarget="target"
// width={canvasWidth}
// height={canvasHeight}
// backgroundColor="transparent"
// nodeCanvasObject={paintNode}
// linkWidth={0.5}
// nodeLabel=""
// dagMode="radialout"
// linkColor={() => isDark ? '#2C2D3A' : 'rgba(0, 0, 0, 0.2)'}
// onNodeClick={handleNodeClick}
// enableZoomInteraction={false}
// enablePanInteraction={false}
// dagLevelDistance={100}
// warmupTicks={500}
// d3VelocityDecay={0.75}
// onEngineTick={handleEngineTick}
// nodePointerAreaPaint={paintPointerArea}
// />
// ) : (
// <div>Loading...</div>
// )}
// </div>
// );
// };
// export default React.memo(FileKindStatistics);

View file

@ -1,99 +1,274 @@
import { Info } from '@phosphor-icons/react';
import { getIcon } from '@sd/assets/util';
import clsx from 'clsx';
import { motion } from 'framer-motion';
import { useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { formatNumber, SearchFilterArgs, useLibraryQuery } from '@sd/client';
import { Icon } from '~/components';
import { useLocale } from '~/hooks';
import React, { MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router';
import { KindStatistic, uint32ArrayToBigInt, useLibraryQuery } from '@sd/client';
import { Card, Tooltip } from '@sd/ui';
import { useIsDark, useLocale } from '~/hooks';
import { translateKindName } from '../Explorer/util';
const INFO_ICON_CLASSLIST =
'inline size-3 text-ink-faint opacity-0 ml-1 transition-opacity duration-300 group-hover:opacity-70';
const TOTAL_FILES_CLASSLIST =
'flex items-center justify-between whitespace-nowrap text-sm font-medium text-ink-dull mt-2 px-1';
const UNIDENTIFIED_FILES_CLASSLIST = 'relative flex items-center text-xs text-ink-faint';
const BARS_CONTAINER_CLASSLIST =
'relative mx-2.5 grid grow grid-cols-[repeat(auto-fit,_minmax(0,_1fr))] grid-rows-[136px_12px] items-end justify-items-center gap-x-1.5 gap-y-1 self-stretch';
export default () => {
const ref = useRef<HTMLDivElement>(null);
const kinds = useLibraryQuery(['library.kindStatistics']);
return (
<>
{/* This is awful, will replace icons accordingly and memo etc */}
{kinds.data?.statistics
?.sort((a, b) => b.count - a.count)
.filter((i) => i.kind !== 0)
.map(({ kind, name, count }) => {
let icon = name;
switch (name) {
case 'Code':
icon = 'Terminal';
break;
case 'Unknown':
icon = 'Undefined';
break;
}
return (
<motion.div
viewport={{
root: ref,
// WARNING: Edge breaks if the values are not postfixed with px or %
margin: '0% -120px 0% 0%'
}}
className={clsx('min-w-fit')}
key={kind}
>
<KindItem
kind={kind}
name={translateKindName(name)}
icon={icon}
items={count}
onClick={() => {}}
/>
</motion.div>
);
})}
</>
);
const mapFractionalValue = (numerator: bigint, denominator: bigint, maxValue: bigint): string => {
if (denominator === 0n) return '0';
const result = (numerator * maxValue) / denominator;
// ensures min width except for empty bars (numerator = 0)
if (numerator != 0n && result < 1) return '1';
return result.toString();
};
interface KindItemProps {
kind: number;
name: string;
items: number;
icon: string;
selected?: boolean;
onClick?: () => void;
disabled?: boolean;
const formatNumberWithCommas = (number: number | bigint) => number.toLocaleString();
const interpolateHexColor = (color1: string, color2: string, factor: number): string => {
const hex = (color: string) => parseInt(color.slice(1), 16);
const r = Math.round((1 - factor) * (hex(color1) >> 16) + factor * (hex(color2) >> 16));
const g = Math.round(
(1 - factor) * ((hex(color1) >> 8) & 0x00ff) + factor * ((hex(color2) >> 8) & 0x00ff)
);
const b = Math.round(
(1 - factor) * (hex(color1) & 0x0000ff) + factor * (hex(color2) & 0x0000ff)
);
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
};
interface FileKind {
kind: string;
count: bigint;
id: number;
}
const KindItem = ({ kind, name, icon, items, selected, onClick, disabled }: KindItemProps) => {
interface FileKindStatsProps {}
const defaultFileKinds: FileKind[] = [
{ kind: 'Package', count: 0n, id: 4 },
{ kind: 'Archive', count: 0n, id: 8 },
{ kind: 'Executable', count: 0n, id: 9 },
{ kind: 'Encrypted', count: 0n, id: 11 },
{ kind: 'Key', count: 0n, id: 12 },
{ kind: 'Link', count: 0n, id: 13 },
{ kind: 'WebPageArchive', count: 0n, id: 14 },
{ kind: 'Widget', count: 0n, id: 15 },
{ kind: 'Album', count: 0n, id: 16 },
{ kind: 'Collection', count: 0n, id: 17 },
{ kind: 'Font', count: 0n, id: 18 },
{ kind: 'Mesh', count: 0n, id: 19 },
{ kind: 'Code', count: 0n, id: 20 },
{ kind: 'Database', count: 0n, id: 21 },
{ kind: 'Book', count: 0n, id: 22 },
{ kind: 'Config', count: 0n, id: 23 },
{ kind: 'Dotfile', count: 0n, id: 24 },
{ kind: 'Screenshot', count: 0n, id: 25 }
];
const FileKindStats: React.FC<FileKindStatsProps> = () => {
const isDark = useIsDark();
const navigate = useNavigate();
const { t } = useLocale();
return (
<Link
to={{
const { data } = useLibraryQuery(['library.kindStatistics']);
const [fileKinds, setFileKinds] = useState<FileKind[]>([]);
const [cardWidth, setCardWidth] = useState<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
const iconsRef = useRef<{ [key: string]: HTMLImageElement }>({});
const BAR_MAX_HEIGHT = 115n;
const BAR_COLOR_START = '#3A7ECC';
const BAR_COLOR_END = '#004C99';
const formatCount = (count: number | bigint): string => {
const bigIntCount = typeof count === 'number' ? BigInt(count) : count;
return bigIntCount >= 1000n ? `${bigIntCount / 1000n}K` : count.toString();
};
const handleResize = useCallback(() => {
if (containerRef.current) {
const factor = window.innerWidth > 1500 ? 0.35 : 0.4;
setCardWidth(window.innerWidth * factor);
}
}, []);
useEffect(() => {
window.addEventListener('resize', handleResize);
handleResize();
const containerElement = containerRef.current;
if (containerElement) {
const observer = new MutationObserver(handleResize);
observer.observe(containerElement, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['style']
});
return () => {
observer.disconnect();
};
}
return () => {
window.removeEventListener('resize', handleResize);
};
}, [handleResize]);
useEffect(() => {
if (data) {
const statistics: KindStatistic[] = data.statistics
.filter(
(item: { kind: number; count: any }) => uint32ArrayToBigInt(item.count) !== 0n
)
.sort((a: { count: any }, b: { count: any }) => {
const aCount = uint32ArrayToBigInt(a.count);
const bCount = uint32ArrayToBigInt(b.count);
if (aCount === bCount) return 0;
return aCount > bCount ? -1 : 1;
});
setFileKinds(
statistics.map((item) => ({
kind: item.name,
count: uint32ArrayToBigInt(item.count),
id: item.kind
}))
);
if (statistics.length < 10) {
const additionalKinds = defaultFileKinds.filter(
(defaultKind) => !statistics.some((stat) => stat.kind === defaultKind.id)
);
const kindsToAdd = additionalKinds.slice(0, 10 - statistics.length);
setFileKinds((prevKinds) => [...prevKinds, ...kindsToAdd]);
}
data.statistics.forEach((item: { name: string }) => {
const iconName = item.name;
if (!iconsRef.current[iconName]) {
const img = new Image();
img.src = getIcon(iconName + '20', isDark);
iconsRef.current[iconName] = img;
}
});
}
}, [data, isDark]);
const sortedFileKinds = [...fileKinds].sort((a, b) => {
if (a.count === b.count) return 0;
return a.count > b.count ? -1 : 1;
});
const maxFileCount = sortedFileKinds && sortedFileKinds[0] ? sortedFileKinds[0].count : 0n;
const barCount = sortedFileKinds.length;
const makeBarClickHandler =
(fileKind: FileKind): MouseEventHandler<HTMLDivElement> | undefined =>
() => {
const path = {
pathname: '../search',
search: new URLSearchParams({
filters: JSON.stringify([
{ object: { kind: { in: [kind] } } }
] as SearchFilterArgs[])
filters: JSON.stringify([{ object: { kind: { in: [fileKind.id] } } }])
}).toString()
}}
>
<div
onClick={onClick}
className={clsx(
'flex shrink-0 items-center rounded-lg py-1 text-sm outline-none focus:bg-app-selectedItem/50',
selected && 'bg-app-selectedItem',
disabled && 'cursor-not-allowed opacity-30'
)}
};
navigate(path);
};
return (
<div className="flex justify-center">
<Card
ref={containerRef}
className="max-w-1/2 group mx-1 flex h-[220px] w-full min-w-[400px] shrink-0 flex-col gap-2 bg-app-box/50"
>
<Icon name={icon as any} className="mr-3 size-12" />
<div className="pr-5">
<h2 className="text-sm font-medium">{name}</h2>
{items !== undefined && (
<p className="text-xs text-ink-faint">
{t('item_with_count', { count: items })}
</p>
)}
<div className={TOTAL_FILES_CLASSLIST}>
<Tooltip className="flex items-center" label={t('bar_graph_info')}>
<div className="flex items-center gap-2">
<span
className={clsx(
'text-xl font-black',
isDark ? 'text-white' : 'text-black'
)}
>
{data?.total_identified_files
? formatNumberWithCommas(data.total_identified_files)
: '0'}{' '}
</span>
<div className="flex items-center">
{t('total_files')}
<Info weight="fill" className={INFO_ICON_CLASSLIST} />
</div>
</div>
</Tooltip>
<div className={UNIDENTIFIED_FILES_CLASSLIST}>
<Tooltip label={t('unidentified_files_info')}>
<span>
{data?.total_unidentified_files
? formatNumberWithCommas(data.total_unidentified_files)
: '0'}{' '}
unidentified files
</span>
</Tooltip>
</div>
</div>
</div>
</Link>
<div className={BARS_CONTAINER_CLASSLIST}>
{sortedFileKinds.map((fileKind, index) => {
const iconImage = iconsRef.current[fileKind.kind];
const barColor = interpolateHexColor(
BAR_COLOR_START,
BAR_COLOR_END,
index / (barCount - 1)
);
const barHeight =
mapFractionalValue(fileKind.count, maxFileCount, BAR_MAX_HEIGHT) + 'px';
return (
<>
<Tooltip
asChild
key={fileKind.kind}
label={
formatNumberWithCommas(fileKind.count) +
' ' +
fileKind.kind +
's'
}
position="left"
>
<div
className="relative flex w-full min-w-8 max-w-10 grow cursor-pointer flex-col items-center"
onDoubleClick={makeBarClickHandler(fileKind)}
>
{iconImage && (
<img
src={iconImage.src}
alt={fileKind.kind}
className="relative mb-1 size-4 duration-500"
/>
)}
<motion.div
className="flex w-full flex-col items-center rounded transition-all duration-500"
initial={{ height: 0 }}
animate={{ height: barHeight }}
transition={{ duration: 0.4, ease: [0.42, 0, 0.58, 1] }}
style={{
height: barHeight,
backgroundColor: barColor
}}
></motion.div>
</div>
</Tooltip>
<div className="sm col-span-1 row-start-2 row-end-auto text-center text-[10px] font-medium text-ink-faint">
{formatCount(fileKind.count)}
</div>
</>
);
})}
</div>
</Card>
</div>
);
};
export default FileKindStats;

View file

@ -2,8 +2,10 @@ import { Info } from '@phosphor-icons/react';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { humanizeSize, Statistics, useLibraryContext, useLibraryQuery } from '@sd/client';
import { Tooltip } from '@sd/ui';
import { useCounter, useLocale } from '~/hooks';
import { Card, Tooltip } from '@sd/ui';
import { useCounter, useIsDark, useLocale } from '~/hooks';
import StorageBar from './StorageBar';
interface StatItemProps {
title: string;
@ -12,15 +14,18 @@ interface StatItemProps {
info?: string;
}
interface Section {
name: string;
value: number;
color: string;
tooltip: string;
}
let mounted = false;
const StatItem = (props: StatItemProps) => {
const { title, bytes, isLoading } = props;
// This is important to the counter working.
// On first render of the counter this will potentially be `false` which means the counter should the count up.
// but in a `useEffect` `mounted` will be set to `true` so that subsequent renders of the counter will not run the count up.
// The acts as a cache of the value of `mounted` on the first render of this `StateItem`.
const [isMounted] = useState(mounted);
const size = humanizeSize(bytes);
@ -36,7 +41,7 @@ const StatItem = (props: StatItemProps) => {
return (
<div
className={clsx(
'group/stat flex w-32 shrink-0 flex-col duration-75',
'group/stat flex w-36 shrink-0 flex-col duration-75',
!bytes && 'hidden'
)}
>
@ -46,7 +51,7 @@ const StatItem = (props: StatItemProps) => {
<Tooltip label={props.info}>
<Info
weight="fill"
className="-mt-0.5 ml-1 inline size-3 text-ink-faint opacity-0 transition-opacity group-hover/stat:opacity-70"
className="-mt-0.5 ml-1 inline size-3 text-ink-faint opacity-0 transition-opacity duration-300 group-hover/stat:opacity-70"
/>
</Tooltip>
)}
@ -69,42 +74,99 @@ const StatItem = (props: StatItemProps) => {
};
const LibraryStats = () => {
const isDark = useIsDark();
const { library } = useLibraryContext();
const stats = useLibraryQuery(['library.statistics']);
const storageBarData = useLibraryQuery(['library.kindStatistics']).data?.statistics;
const { t } = useLocale();
useEffect(() => {
if (!stats.isLoading) mounted = true;
});
const { t } = useLocale();
}, [stats.isLoading]);
const StatItemNames: Partial<Record<keyof Statistics, string>> = {
total_library_bytes: t('library_bytes'),
library_db_size: t('library_db_size'),
total_local_bytes_capacity: t('total_bytes_capacity'),
total_library_preview_media_bytes: t('preview_media_bytes'),
total_local_bytes_free: t('total_bytes_free'),
total_local_bytes_used: t('total_bytes_used')
library_db_size: t('library_db_size'),
total_library_preview_media_bytes: t('preview_media_bytes')
};
const StatDescriptions: Partial<Record<keyof Statistics, string>> = {
total_local_bytes_capacity: t('total_bytes_capacity_description'),
total_library_preview_media_bytes: t('preview_media_bytes_description'),
total_library_bytes: t('library_bytes_description'),
library_db_size: t('library_db_size_description'),
total_local_bytes_capacity: t('total_bytes_capacity_description'),
total_local_bytes_free: t('total_bytes_free_description'),
total_local_bytes_used: t('total_bytes_used_description')
library_db_size: t('library_db_size_description'),
total_library_preview_media_bytes: t('preview_media_bytes_description')
};
const displayableStatItems = Object.keys(
StatItemNames
) as unknown as keyof typeof StatItemNames;
if (!stats.data || !stats.data.statistics) {
return <div>Loading...</div>;
}
const { statistics } = stats.data;
const totalSpace = Number(statistics.total_local_bytes_capacity);
const totalUsedSpace = Number(statistics.total_local_bytes_used);
// Define the major categories and aggregate the "Other" category
const majorCategories = ['Document', 'Text', 'Image', 'Video'];
const aggregatedData = (storageBarData ?? []).reduce(
(acc, curr) => {
const category = majorCategories.includes(curr.name) ? curr.name : 'Other';
if (!acc[category]) {
acc[category] = { total_bytes: 0 };
}
acc[category]!.total_bytes += curr.total_bytes[1];
return acc;
},
{} as Record<string, { total_bytes: number }>
);
// Calculate the used space and determine the System Data
const usedSpace = Object.values(aggregatedData).reduce(
(acc, curr) => acc + curr.total_bytes,
0
);
const systemDataBytes = totalUsedSpace - usedSpace;
if (!aggregatedData['Other']) {
aggregatedData['Other'] = { total_bytes: 0 };
}
const sections: Section[] = Object.entries(aggregatedData).map(([name, data], index) => {
const colors = [
'#3A7ECC', // Slightly Darker Blue 400
'#AAAAAA', // Gray
'#004C99', // Tailwind Blue 700
'#2563EB', // Tailwind Blue 500
'#00274D' // Dark Navy Blue,
];
const color = colors[index % colors.length] || '#8F8F8F'; // Use a default color if colors array is empty
return {
name,
value: data.total_bytes,
color,
tooltip: `${name}`
};
});
// Add System Data section
sections.push({
name: 'System Data',
value: systemDataBytes,
color: '#2F3038', // Gray for System Data
tooltip: 'System data that exists outside of your Spacedrive library'
});
return (
<div className="flex w-full">
<div className="flex gap-3 overflow-hidden">
{Object.entries(stats?.data?.statistics || [])
// sort the stats by the order of the displayableStatItems
<Card className="flex h-[220px] w-[750px] shrink-0 flex-col bg-app-box/50">
<div className="mb-1 flex overflow-hidden p-4">
{Object.entries(statistics)
.sort(
([a], [b]) =>
displayableStatItems.indexOf(a) - displayableStatItems.indexOf(b)
@ -115,14 +177,17 @@ const LibraryStats = () => {
<StatItem
key={`${library.uuid} ${key}`}
title={StatItemNames[key as keyof Statistics]!}
bytes={BigInt(value)}
bytes={BigInt(value as number)}
isLoading={stats.isLoading}
info={StatDescriptions[key as keyof Statistics]}
/>
);
})}
</div>
</div>
<div>
<StorageBar sections={sections} totalSpace={totalSpace} />
</div>
</Card>
);
};

View file

@ -20,14 +20,22 @@ const StatCard = ({ icon, name, connectionType, ...stats }: StatCardProps) => {
const isDark = useIsDark();
//TODO: Improve this
const totalSpaceSingleValue = humanizeSize(stats.totalSpace);
const { totalSpace, freeSpace, usedSpaceSpace } = useMemo(() => {
const totalSpace = humanizeSize(stats.totalSpace);
const totalSpace = humanizeSize(stats.totalSpace, {
no_thousands: false
});
const freeSpace = stats.freeSpace == null ? totalSpace : humanizeSize(stats.freeSpace);
return {
totalSpace,
freeSpace,
usedSpaceSpace: humanizeSize(totalSpace.bytes - freeSpace.bytes)
};
}, [stats]);
useEffect(() => {
@ -50,7 +58,7 @@ const StatCard = ({ icon, name, connectionType, ...stats }: StatCardProps) => {
progress={progress}
strokeWidth={6}
trackStrokeWidth={6}
strokeColor={progress > 90 ? '#E14444' : '#2599FF'}
strokeColor={progress >= 90 ? '#E14444' : progress >= 75 ? 'darkorange' : progress >= 60 ? 'yellow' : '#2599FF'}
fillColor="transparent"
trackStrokeColor={isDark ? '#252631' : '#efefef'}
strokeLinecap="square"
@ -76,7 +84,7 @@ const StatCard = ({ icon, name, connectionType, ...stats }: StatCardProps) => {
{freeSpace.value !== totalSpace.value && (
<>
{freeSpace.value} {t(`size_${freeSpace.unit.toLowerCase()}`)}{' '}
{t('free_of')} {totalSpace.value}{' '}
{t('free_of')} {totalSpaceSingleValue.value}{' '}
{t(`size_${totalSpace.unit.toLowerCase()}`)}
</>
)}

View file

@ -0,0 +1,129 @@
import React, { useState } from 'react';
import { humanizeSize } from '@sd/client';
import { Tooltip } from '@sd/ui';
import { useIsDark } from '~/hooks';
const BARWIDTH = 700;
const lightenColor = (color: string, percent: number) => {
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) + amt;
const G = ((num >> 8) & 0x00ff) + amt;
const B = (num & 0x0000ff) + amt;
return `#${(
0x1000000 +
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
(B < 255 ? (B < 1 ? 0 : B) : 255)
)
.toString(16)
.slice(1)
.toUpperCase()}`;
};
interface Section {
name: string;
value: number;
color: string;
tooltip: string;
}
interface StorageBarProps {
sections: Section[];
totalSpace: number;
}
const StorageBar: React.FC<StorageBarProps> = ({ sections, totalSpace }) => {
const isDark = useIsDark();
const [hoveredSectionIndex, setHoveredSectionIndex] = useState<number | null>(null);
const getPercentage = (value: number) => {
const percentage = value / totalSpace;
const pixvalue = BARWIDTH * percentage;
return `${pixvalue.toFixed(2)}px`;
};
const usedSpace = sections.reduce((acc, section) => acc + section.value, 0);
const unusedSpace = totalSpace - usedSpace;
const nonSystemSections = sections.filter((section) => section.name !== 'System Data');
const systemSection = sections.find((section) => section.name === 'System Data');
return (
<div className="w-auto p-3">
<div className="relative mt-1 flex h-6 overflow-hidden rounded">
{nonSystemSections.map((section, index) => {
const humanizedValue = humanizeSize(section.value);
const isHovered = hoveredSectionIndex === index;
return (
<Tooltip
key={index}
label={`${humanizedValue.value} ${humanizedValue.unit}`}
position="top"
>
<div
className={`relative h-full`}
style={{
width: getPercentage(section.value),
minWidth: '2px', // Ensure very small sections are visible
backgroundColor: isHovered
? lightenColor(section.color, 30)
: section.color,
transition: 'background-color 0.3s ease-in-out'
}}
onMouseEnter={() => setHoveredSectionIndex(index)}
onMouseLeave={() => setHoveredSectionIndex(null)}
/>
</Tooltip>
);
})}
{unusedSpace > 0 && (
<div
className="relative h-full"
style={{
width: getPercentage(unusedSpace),
backgroundColor: isDark ? '#1C1D25' : '#D3D3D3'
}}
/>
)}
{systemSection && (
<Tooltip
label={`${humanizeSize(systemSection.value).value} ${humanizeSize(systemSection.value).unit}`}
position="top"
>
<div
className="relative h-full rounded-r"
style={{
width: getPercentage(systemSection.value),
minWidth: '2px',
backgroundColor: systemSection.color,
transition: 'background-color 0.3s ease-in-out'
}}
/>
</Tooltip>
)}
</div>
<div className={`mt-6 flex flex-wrap ${isDark ? 'text-ink-dull' : 'text-gray-800'}`}>
{sections.map((section, index) => (
<Tooltip key={index} label={section.tooltip} position="top">
<div
className="mb-2 mr-6 flex items-center"
onMouseEnter={() => setHoveredSectionIndex(index)}
onMouseLeave={() => setHoveredSectionIndex(null)}
>
<span
className="mr-2 inline-block size-2 rounded-full"
style={{ backgroundColor: section.color }}
/>
<span className="text-sm">{section.name}</span>
</div>
</Tooltip>
))}
</div>
</div>
);
};
export default StorageBar;

View file

@ -76,10 +76,9 @@ export const Component = () => {
<div className="mt-4 flex flex-col gap-3 pt-3">
<OverviewSection>
<LibraryStatistics />
</OverviewSection>
<OverviewSection>
<FileKindStatistics />
</OverviewSection>
<OverviewSection count={1} title={t('devices')}>
{node && (
<StatisticItem
@ -93,46 +92,6 @@ export const Component = () => {
connectionType={null}
/>
)}
{/* <StatisticItem
name="Jamie's MacBook"
icon="Laptop"
total_space="4098046511104"
free_space="969004651119"
color="#0362FF"
connection_type="p2p"
/>
<StatisticItem
name="Jamie's iPhone"
icon="Mobile"
total_space="500046511104"
free_space="39006511104"
color="#0362FF"
connection_type="p2p"
/>
<StatisticItem
name="Titan NAS"
icon="Server"
total_space="60000046511104"
free_space="43000046511104"
color="#0362FF"
connection_type="p2p"
/>
<StatisticItem
name="Jamie's iPad"
icon="Tablet"
total_space="1074077906944"
free_space="121006553275"
color="#0362FF"
connection_type="lan"
/>
<StatisticItem
name="Jamie's Air"
icon="Laptop"
total_space="4098046511104"
free_space="969004651119"
color="#0362FF"
connection_type="p2p"
/> */}
<NewCard
icons={['Laptop', 'Server', 'SilverBox', 'Tablet']}
text={t('connect_device_description')}
@ -164,23 +123,6 @@ export const Component = () => {
</OverviewSection>
<OverviewSection count={0} title={t('cloud_drives')}>
{/* <StatisticItem
name="James Pine"
icon="DriveDropbox"
total_space="104877906944"
free_space="074877906944"
color="#0362FF"
connection_type="cloud"
/>
<StatisticItem
name="Spacedrive S3"
icon="DriveAmazonS3"
total_space="1074877906944"
free_space="704877906944"
color="#0362FF"
connection_type="cloud"
/> */}
<NewCard
icons={[
'DriveAmazonS3',
@ -193,22 +135,6 @@ export const Component = () => {
// buttonText={t('connect_cloud)}
/>
</OverviewSection>
{/* <OverviewSection title="Locations">
<div className="flex flex-row gap-2">
{locations.map((location) => (
<div
key={location.id}
className="flex w-[100px] flex-col items-center gap-2"
>
<Icon size={80} name="Folder" />
<span className="text-xs font-medium truncate">
{location.name}
</span>
</div>
))}
</div>
</OverviewSection> */}
</div>
</div>
</SearchContextProvider>

View file

@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { ObjectOrder, objectOrderingKeysSchema } from '@sd/client';
import { Icon } from '~/components';
import { useLocale, useRouteTitle } from '~/hooks';
@ -17,6 +17,8 @@ import { TopBarPortal } from './TopBar/Portal';
export function Component() {
useRouteTitle('Recents');
const [_, setForceRender] = useState(false);
const explorerSettings = useExplorerSettings({
settings: useMemo(() => {
return createDefaultExplorerSettings<ObjectOrder>({ order: null });
@ -48,6 +50,12 @@ export function Component() {
settings: explorerSettings
});
//this forces a re-render so that the explorer can update and show the objects
//since this is a recents page issue only - this is sufficient unless otherwise
useEffect(() => {
setForceRender((prev) => !prev);
}, [items.query.isFetching]);
return (
<ExplorerContextProvider explorer={explorer}>
<SearchContextProvider search={search}>
@ -68,8 +76,7 @@ export function Component() {
)}
</TopBarPortal>
</SearchContextProvider>
<Explorer
<Explorer
emptyNotice={
<EmptyNotice
icon={<Icon name="Collection" size={128} />}

View file

@ -1,4 +1,6 @@
{
"Connect": "يتصل",
"Connecting": "توصيل",
"about": "حول",
"about_vision_text": "العديد منا لديه حسابات سحابية متعددة، ومحركات أقراص غير محفوظة وبيانات معرضة للخطر من الفقدان. نعتمد على خدمات السحاب مثل Google Photos و iCloud ، ولكننا مقيدين بسعة محدودة وتقريبًا لا توجد توافقية بين الخدمات وأنظمة التشغيل. لا ينبغي أن تكون ألبومات الصور محبوسة في نظام الجهاز أو تستخدم لجمع البيانات الإعلانية. يجب أن تكون غير معتمدة على نظام التشغيل ، دائمة ومملوكة شخصيًا. البيانات التي ننشئها هي إرثنا ، والذي سيستمر لفترة طويلة بعد وفاتنا - التكنولوجيا مفتوحة المصدر هي الطريقة الوحيدة لضمان السيطرة المطلقة على البيانات التي تحدد حياتنا ، بحجم غير محدود.",
"about_vision_title": "الرؤية",
@ -69,6 +71,7 @@
"close_command_palette": "إغلاق لوحة الأوامر",
"close_current_tab": "إغلاق العلامة التبويب الحالية",
"cloud": "Cloud",
"cloud_connect_description": "هل ترغب في ربط مكتبتك بالسحابة؟",
"cloud_drives": "Cloud Drives",
"cloud_sync": "مزامنة السحابة",
"cloud_sync_description": "إدارة العمليات التي تزامن مكتبتك مع Spacedrive Cloud",

View file

@ -1,4 +1,6 @@
{
"Connect": "Злучыцца",
"Connecting": "Падключэнне",
"about": "Аб Spacedrive",
"about_vision_text": "У многіх з нас ёсць некалькі уліковых запісаў у воблаку, дыскі без рэзервовай копіі і дадзеныя, якія могуць быць згубленыя. Мы залежым ад хмарных сэрвісаў, такіх як Google Photos і iCloud, але іх магчымасці абмежаваныя, а сумяшчальнасць паміж сэрвісамі і аперацыйнымі сістэмамі практычна адсутнічае. Фотагалерэя не павінна быць прывязаная да экасістэме прылады або выкарыстоўвацца для збору рэкламных дадзеных. Яна павінна быць незалежная ад аперацыйнай сістэмы, сталая і належаць асабіста вам. Дадзеныя, якія мы ствараем, - гэта наша спадчына, якое надоўга перажыве нас. Тэхналогія з адкрытым зыходным кодам-адзіны спосаб забяспечыць абсалютны кантроль над дадзенымі, якія вызначаюць наша жыццё, у неабмежаванай маштабе.",
"about_vision_title": "Бачанне",
@ -69,6 +71,7 @@
"close_command_palette": "Закрыць панэль каманд",
"close_current_tab": "Зачыніць бягучую ўкладку",
"cloud": "Воблака",
"cloud_connect_description": "Хочаце падключыць сваю бібліятэку да воблака?",
"cloud_drives": "Воблачныя дыскі",
"cloud_sync": "Воблачная сінхранізацыя",
"cloud_sync_description": "Кіруйце працэсамі, якія сінхранізуюць вашу бібліятэку з Spacedrive Cloud",

View file

@ -1,4 +1,6 @@
{
"Connect": "Verbinden",
"Connecting": "Verbinden",
"about": "Über",
"about_vision_text": "Viele von uns haben mehrere Cloud-Konten, Laufwerke, die nicht gesichert sind, und Daten, die von Verlust bedroht sind. Wir verlassen uns auf Cloud-Dienste wie Google Fotos und iCloud, sind aber mit begrenzter Kapazität eingesperrt und haben fast keine Interoperabilität zwischen Diensten und Betriebssystemen. Fotoalben sollten nicht in einem Geräte-Ökosystem feststecken oder für Werbedaten geerntet werden. Du solltest betriebssystemunabhängig, dauerhaft und persönlich besessen sein. Daten, die wir erstellen, sind unser Erbe, das uns lange überleben wird - Open-Source-Technologie ist der einzige Weg, um sicherzustellen, dass wir absolute Kontrolle über die Daten behalten, die unser Leben definieren, in unbegrenztem Maßstab.",
"about_vision_title": "Vision",
@ -69,6 +71,7 @@
"close_command_palette": "Befehlspalette schließen",
"close_current_tab": "Aktuellen Tab schließen",
"cloud": "Cloud",
"cloud_connect_description": "Möchten Sie Ihre Bibliothek mit der Cloud verbinden?",
"cloud_drives": "Cloud-Laufwerke",
"cloud_sync": "Cloud-Synchronisierung",
"cloud_sync_description": "Verwalten Sie die Prozesse, die Ihre Bibliothek mit Spacedrive Cloud synchronisieren",

View file

@ -1,5 +1,7 @@
{
"Add Device Description": "Scan the QR code or authenticate your device UUID to add a device.",
"Connect": "Connect",
"Connecting": "Connecting",
"about": "About",
"about_vision_text": "Many of us have multiple cloud accounts, drives that arent backed up and data at risk of loss. We depend on cloud services like Google Photos and iCloud, but are locked in with limited capacity and almost zero interoperability between services and operating systems. Photo albums shouldnt be stuck in a device ecosystem, or harvested for advertising data. They should be OS agnostic, permanent and personally owned. Data we create is our legacy, that will long outlive us—open source technology is the only way to ensure we retain absolute control over the data that defines our lives, at unlimited scale.",
"about_vision_title": "Vision",
@ -48,6 +50,7 @@
"backfill_sync_description": "Library is paused until backfill completes",
"backups": "Backups",
"backups_description": "Manage your Spacedrive database backups.",
"bar_graph_info": "Hover over each bar to see the file type. Double click to navigate.",
"bitrate": "Bitrate",
"blur_effects": "Blur Effects",
"blur_effects_description": "Some components will have a blur effect applied to them.",
@ -70,6 +73,7 @@
"close_command_palette": "Close command palette",
"close_current_tab": "Close current tab",
"cloud": "Cloud",
"cloud_connect_description": "Would you like to connect your library to the cloud?",
"cloud_drives": "Cloud Drives",
"cloud_sync": "Cloud Sync",
"cloud_sync_description": "Manage the processes that sync your library with Spacedrive Cloud",
@ -677,8 +681,9 @@
"tags_bulk_assigned": "Assigned tag \"{{tag_name}}\" to {{file_count}} $t(file, { \"count\": {{file_count}} }).",
"tags_bulk_failed_with_tag": "Could not assign tag \"{{tag_name}}\" to {{file_count}} $t(file, { \"count\": {{file_count}} }): {{error_message}}",
"tags_bulk_failed_without_tag": "Could not tag {{file_count}} $t(file, { \"count\": {{file_count}} }): {{error_message}}",
"tags_bulk_instructions": "Select one or more files and press a key to assign the corresponding tag.",
"tags_bulk_instructions": "Select one or more files and press a number key to assign/unassign the corresponding tag.",
"tags_bulk_mode_active": "Tag assign mode is enabled.",
"tags_bulk_unassigned": "Unassigned tag \"{{tag_name}}\" to {{file_count}} $t(file, { \"count\": {{file_count}} }).",
"tags_description": "Manage your tags.",
"tags_notice_message": "No items assigned to this tag.",
"task": "task",
@ -711,10 +716,12 @@
"total_bytes_free_description": "Free space available on all nodes connected to the library.",
"total_bytes_used": "Total used space",
"total_bytes_used_description": "Total space used on all nodes connected to the library.",
"total_files": "Total files",
"trash": "Trash",
"type": "Type",
"ui_animations": "UI Animations",
"ui_animations_description": "Dialogs and other UI elements will animate when opening and closing.",
"unidentified_files_info": "Files that Spacedrive has been unable to identify.",
"unknown": "Unknown",
"unnamed_location": "Unnamed Location",
"update": "Update",

View file

@ -1,4 +1,6 @@
{
"Connect": "Conectar",
"Connecting": "Conectando",
"about": "Acerca de",
"about_vision_text": "Muchos de nosotros tenemos múltiples cuentas en la nube, discos que no tienen copias de seguridad y datos en riesgo de ser perdidos. Dependemos de servicios en la nube como Google Photos e iCloud, pero estamos limitados con una capacidad reducida y casi cero interoperabilidad entre servicios y sistemas operativos. Los álbumes de fotos no deberían estar atascados en un ecosistema de dispositivos o ser utilizados para datos publicitarios. Deberían ser independientes del SO, permanentes y de propiedad personal. Los datos que creamos son nuestro legado, que nos sobrevivirá mucho tiempo: la tecnología de código abierto es la única forma de asegurarnos de mantener el control absoluto sobre los datos que definen nuestras vidas, en una escala ilimitada.",
"about_vision_title": "Visión",
@ -69,6 +71,7 @@
"close_command_palette": "Cerrar paleta de comandos",
"close_current_tab": "Cerrar pestaña actual",
"cloud": "Nube",
"cloud_connect_description": "¿Le gustaría conectar su biblioteca a la nube?",
"cloud_drives": "Unidades en la nube",
"cloud_sync": "Sincronización en la nube",
"cloud_sync_description": "Gestiona los procesos que sincronizan tu biblioteca con Spacedrive Cloud",

View file

@ -1,4 +1,6 @@
{
"Connect": "Connecter",
"Connecting": "De liaison",
"about": "À propos",
"about_vision_text": "Beaucoup d'entre nous ont plusieurs comptes cloud, des disques qui ne sont pas sauvegardés et des données à risque de perte. Nous dépendons de services cloud comme Google Photos et iCloud, mais sommes enfermés avec une capacité limitée et presque zéro interopérabilité entre les services et les systèmes d'exploitation. Les albums photo ne devraient pas être coincés dans un écosystème d'appareils, ou récoltés pour des données publicitaires. Ils devraient être indépendants du système d'exploitation, permanents et personnels. Les données que nous créons sont notre héritage, qui nous survivra longtemps - la technologie open source est le seul moyen d'assurer que nous conservons un contrôle absolu sur les données qui définissent nos vies, à une échelle illimitée.",
"about_vision_title": "Vision",
@ -69,6 +71,7 @@
"close_command_palette": "Fermer la palette de commandes",
"close_current_tab": "Fermer l'onglet actuel",
"cloud": "Nuage",
"cloud_connect_description": "Souhaitez-vous connecter votre bibliothèque au cloud ?",
"cloud_drives": "Lecteurs en nuage",
"cloud_sync": "Synchronisation dans le cloud",
"cloud_sync_description": "Gérez les processus qui synchronisent votre bibliothèque avec Spacedrive Cloud",

View file

@ -1,4 +1,6 @@
{
"Connect": "Collegare",
"Connecting": "Connessione",
"about": "Su di noi",
"about_vision_text": "Molti di noi hanno più account cloud, dischi di cui non è stato eseguito il backup e dati a rischio di essere persi. Dipendiamo da servizi cloud come Google Photos e iCloud, che sono limitati dalla loro capacità e scarsissima interoperabilità tra servizi e sistemi operativi. Gli album fotografici non dovrebbero essere bloccati in un ecosistema di dispositivi, o essere usati per raccogliere dati. Dovrebbero essere agnostici dal sistema operativo, e detenuti permanentemente e personalmente. I dati che creiamo sono la nostra eredità, che sopravviverà molto più a lungo di noi. La tecnologia open source è l'unico modo per assicurarci di conservare il controllo assoluto sui dati che definisce le nostre vite, a scala illimitata.",
"about_vision_title": "Visione",
@ -69,6 +71,7 @@
"close_command_palette": "Chiudi la tavolozza dei comandi",
"close_current_tab": "Chiudi scheda corrente",
"cloud": "Cloud",
"cloud_connect_description": "Vorresti connettere la tua libreria al cloud?",
"cloud_drives": "Unità cloud",
"cloud_sync": "Sincronizzazione nel cloud",
"cloud_sync_description": "Gestisci i processi che sincronizzano la tua libreria con Spacedrive Cloud",

View file

@ -1,4 +1,6 @@
{
"Connect": "接続する",
"Connecting": "接続中",
"about": "概要",
"about_vision_text": "私達は通常、複数のクラウドのアカウントを持ち、バックアップのないドライブを利用し、データを失う危険にさらされています。またGoogle PhotosやiCloudのようなクラウドサービスに依存していますが、それらは容量に制限があり、サービスやOS間に互換性はほとんどありません。フォトアルバムは、デバイスのエコシステムに縛られたり広告データとして利用されたりすべきではなく、OSにとらわれず、永続的で、個人所有のものであるべきです。私達が作成したデータは私達の遺産であり、私達よりもずっと長生きします。オープンソース・テクロジーは、無制限のスケールで、私達の生活を定義するデータの絶対的なコントロールを確実に保持する唯一の方法なのです。",
"about_vision_title": "ビジョン",
@ -69,6 +71,7 @@
"close_command_palette": "コマンドパレットを閉じる",
"close_current_tab": "タブを閉じる",
"cloud": "クラウド",
"cloud_connect_description": "ライブラリをクラウドに接続しますか?",
"cloud_drives": "クラウドドライブ",
"cloud_sync": "クラウド同期",
"cloud_sync_description": "ライブラリを Spacedrive Cloud と同期するプロセスを管理する",

View file

@ -1,4 +1,6 @@
{
"Connect": "Aansluiten",
"Connecting": "Verbinden",
"about": "Over",
"about_vision_text": "Veel van ons hebben meerdere cloud accounts, schijven waarvan geen back-ups worden gemaakt en gegevens die het risico lopen verloren te gaan. We zijn afhankelijk van clouddiensten als Google Photos en iCloud, maar zitten vast met een beperkte capaciteit en vrijwel geen interoperabiliteit tussen diensten en besturingssystemen. Fotoalbums mogen niet vastzitten in het ecosysteem van een apparaat, of worden gebruikt voor advertentiegegevens. Ze moeten OS-agnostisch, permanent en persoonlijk eigendom zijn. De gegevens die we creëren zijn onze erfenis, die ons nog lang zal overleven. Open source-technologie is de enige manier om ervoor te zorgen dat we de absolute controle behouden over de gegevens die ons leven bepalen, op onbeperkte schaal.",
"about_vision_title": "Visie",
@ -69,6 +71,7 @@
"close_command_palette": "Sluit het opdrachtpalet",
"close_current_tab": "Huidig tabblad sluiten",
"cloud": "Cloud",
"cloud_connect_description": "Wilt u uw bibliotheek verbinden met de cloud?",
"cloud_drives": "Cloud Drives",
"cloud_sync": "Cloudsynchronisatie",
"cloud_sync_description": "Beheer de processen die uw bibliotheek synchroniseren met Spacedrive Cloud",

View file

@ -1,4 +1,6 @@
{
"Connect": "Соединять",
"Connecting": "Подключение",
"about": "О Spacedrive",
"about_vision_text": "У многих из нас есть несколько учетных записей в облаке, диски без резервной копии и данные, которые могут быть утеряны. Мы зависим от облачных сервисов, таких как Google Photos и iCloud, но их возможности ограничены, а совместимость между сервисами и операционными системами практически отсутствует. Фотогалерея не должна быть привязана к экосистеме устройства или использоваться для сбора рекламных данных. Она должна быть независима от операционной системы, постоянна и принадлежать лично Вам. Данные, которые мы создаем, - это наше наследие, которое надолго переживет нас. Технология с открытым исходным кодом - единственный способ обеспечить абсолютный контроль над данными, определяющими нашу жизнь, в неограниченном масштабе.",
"about_vision_title": "Видение",
@ -69,6 +71,7 @@
"close_command_palette": "Закрыть панель команд",
"close_current_tab": "Закрыть текущую вкладку",
"cloud": "Облако",
"cloud_connect_description": "Хотите подключить свою библиотеку к облаку?",
"cloud_drives": "Облачные диски",
"cloud_sync": "Облачная синхронизация",
"cloud_sync_description": "Управляйте процессами синхронизации вашей библиотеки с Spacedrive Cloud",

View file

@ -1,4 +1,6 @@
{
"Connect": "Bağlamak",
"Connecting": "Bağlanıyor",
"about": "Hakkında",
"about_vision_text": "Birçoğumuzun birden fazla bulut hesabı, yedeklenmemiş sürücüleri ve kaybolma riski taşıyan verileri var. Google Fotoğraflar ve iCloud gibi bulut hizmetlerine bağımlıyız, ancak sınırlı kapasiteyle ve hizmetler ile işletim sistemleri arasında neredeyse sıfır geçiş yapabilirlikle kısıtlanmış durumdayız. Fotoğraf albümleri bir cihaz ekosisteminde sıkışıp kalmamalı veya reklam verileri için kullanılmamalıdır. OS bağımsız, kalıcı ve kişisel olarak sahip olunmalıdır. Oluşturduğumuz veriler, bizden uzun süre yaşayacak mirasımızdır - verilerimiz üzerinde mutlak kontrol sağlamak için açık kaynak teknolojisi tek yoludur, sınırsız ölçekte.",
"about_vision_title": "Vizyon",
@ -69,6 +71,7 @@
"close_command_palette": "Komut paletini kapat",
"close_current_tab": "Geçerli sekmeyi kapat",
"cloud": "Bulut",
"cloud_connect_description": "Kütüphanenizi buluta bağlamak ister misiniz?",
"cloud_drives": "Bulut Sürücüler",
"cloud_sync": "Bulut Senkronizasyonu",
"cloud_sync_description": "Kitaplığınızı Spacedrive Cloud ile senkronize eden süreçleri yönetin",

View file

@ -1,6 +1,8 @@
{
"Connect": "连接",
"Connecting": "正在连接",
"about": "关于",
"about_vision_text": "我们很多人拥有不止一个云账户,磁盘没有备份,数据也有丢失的风险。我们依赖于像 Google 相册、iCloud 这样的云服务,但是它们容量有限,且互操作性几乎为零,云服务和操作系统之间也无法协作。我们的照片不应该困在单一一种生态系统中,也不应该被用于广告营销而被收割。它们应该与操作系统无关、永久保存、由我们自己所有。我们创造的数据是我们的遗产,它们的寿命会比我们还要长。开源技术是唯一能确保我们绝对控制定义我们生活的数据,并在无限规模上保留这些定义了我们的生活的数据的方式。",
"about_vision_text": "我们很多人拥有不止一个云账户,磁盘没有备份,数据也有丢失的风险。我们依赖于像 Google 相册、iCloud 这样的云服务,但是它们容量有限,且互操作性几乎为零,云服务和操作系统之间也无法协作。我们的照片不应该困在单一一种生态系统中,也不应该被用于广告营销而被收割它们应该与操作系统无关、永久保存、由我们自己所有。我们创造的数据是我们的遗产,它们的寿命会比我们还要长,无限地定义了我们的生活,而开源技术是唯一能确保我们绝对控制这些数据的方式",
"about_vision_title": "项目远景",
"accept": "接受",
"accept_files": "Accept files",
@ -69,6 +71,7 @@
"close_command_palette": "关闭命令面板",
"close_current_tab": "关闭当前标签页",
"cloud": "雲",
"cloud_connect_description": "您想将您的图书馆连接到云端吗?",
"cloud_drives": "云盘",
"cloud_sync": "云同步",
"cloud_sync_description": "管理将您的库与 Spacedrive Cloud 同步的流程",
@ -128,7 +131,7 @@
"cut": "剪切",
"cut_object": "剪切对象",
"cut_success": "剪切项目",
"dark": "色",
"dark": "色",
"data_folder": "数据文件夹",
"database": "Database",
"date": "日期",
@ -302,7 +305,7 @@
"got_it": "我知道了",
"grid_gap": "间隙",
"grid_view": "网格视图",
"grid_view_notice_description": "通过网格视图直观地了解您的文件。这种视图以缩略图形式显示您的文件和文件夹,方便您快速识别所寻找的文件。",
"grid_view_notice_description": "网格视图以缩略图形式显示文件和文件夹,以便直观、快速识别要寻找的文件。",
"hidden": "隐",
"hidden_label": "阻止位置及其内容出现在汇总分类、搜索和标签中,除非启用了“显示隐藏项目”。",
"hide_in_library_search": "在库搜索中隐藏",
@ -327,7 +330,7 @@
"indexer_rules_error": "Error while retrieving indexer rules",
"indexer_rules_info": "索引器规则允许您使用通配符指定要忽略的路径。",
"indexer_rules_not_available": "No indexer rules available",
"ingester": "摄取者",
"ingester": "同步接收器",
"ingester_description": "此过程接收接收到的云操作并将它们发送到主同步接收器。",
"injester_description": "此过程从 P2P 连接和 Spacedrive Cloud 获取同步操作,并将其应用到库。",
"install": "安装",
@ -360,7 +363,7 @@
"key_manager": "密钥管理器",
"key_manager_description": "创建加密密钥,挂载和卸载密钥以即时查看解密文件。",
"keybinds": "快捷键",
"keybinds_description": "查看管理客户端快捷键",
"keybinds_description": "查看管理客户端快捷键",
"keys": "密钥",
"kilometers": "千米",
"kind": "种类",
@ -641,7 +644,7 @@
"spacedrop_everyone": "所有人",
"spacedrop_rejected": "Spacedrop 被拒绝",
"square_thumbnails": "方形缩略图",
"star_on_github": "在 GitHub 上送一个 star",
"star_on_github": "在 GitHub 上标星",
"start": "开始",
"starting": "开始...",
"starts_with": "以...开始",
@ -678,7 +681,7 @@
"thumbnailer_cpu_usage_description": "限制缩略图生成器在后台处理时可以使用 CPU 的量。",
"to": "到",
"toggle_all": "切换全部",
"toggle_command_palette": "切换命令面板",
"toggle_command_palette": "打开命令面板",
"toggle_hidden_files": "显示/隐藏文件",
"toggle_image_slider_within_quick_preview": "在快速预览中切换图像滑块",
"toggle_inspector": "切换检查器",
@ -686,7 +689,7 @@
"toggle_metadata": "切换元数据",
"toggle_path_bar": "切换显示路径栏",
"toggle_quick_preview": "切换快速预览",
"toggle_sidebar": "切换侧边栏",
"toggle_sidebar": "打开/关闭侧边栏",
"tools": "工具",
"total_bytes_capacity": "总容量",
"total_bytes_capacity_description": "连接到资料库的所有节点的总容量。 在 Alpha 期间可能会显示不正确的值。",
@ -727,4 +730,4 @@
"zoom": "缩放",
"zoom_in": "放大",
"zoom_out": "缩小"
}
}

View file

@ -1,4 +1,6 @@
{
"Connect": "連接",
"Connecting": "連接中",
"about": "關於",
"about_vision_text": "我們中的許多人都擁有數個雲帳戶這些雲帳戶中的硬碟未備份且資料面臨丟失的風險。我們依賴諸如Google照片和iCloud之類的雲服務但這些服務容量有限且幾乎不能在不同的服務及作業系統間進行互通。相簿不應僅限於某個裝置生態系統內或被用於收集廣告數據。它們應該是與作業系統無關永久且屬於個人所有的。我們創建的數據是我們的遺產它們將比我們存活得更久——開源技術是確保我們對定義我們生活的數據擁有絕對控制權的唯一方式並且無限規模地延伸。",
"about_vision_title": "遠景",
@ -69,6 +71,7 @@
"close_command_palette": "關閉命令面板",
"close_current_tab": "關閉目前分頁",
"cloud": "雲",
"cloud_connect_description": "您想將您的圖書館連接到雲端嗎?",
"cloud_drives": "雲端硬碟",
"cloud_sync": "雲端同步",
"cloud_sync_description": "管理將您的庫與 Spacedrive Cloud 同步的流程",

View file

@ -35,6 +35,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"crypto-random-string": "^5.0.0",
"d3-force": "^3.0.0",
"dayjs": "^1.11.10",
"framer-motion": "^10.16.4",
"i18next": "^23.7.10",
@ -46,6 +47,7 @@
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11",
"react-force-graph-2d": "^1.25.5",
"react-hook-form": "^7.47.0",
"react-hotkeys-hook": "^4.4.1",
"react-i18next": "^13.5.0",
@ -71,6 +73,7 @@
},
"devDependencies": {
"@sd/config": "workspace:*",
"@types/d3-force": "^3.0.9",
"@types/node": ">18.18.x",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View file

@ -28,6 +28,8 @@ import Book20 from './Book-20.png';
import Book from './Book.png';
import BookBlue from './BookBlue.png';
import Box from './Box.png';
import CloudSync_Light from './CloudSync_Light.png';
import CloudSync from './CloudSync.png';
import Code20 from './Code-20.png';
import Collection_Light from './Collection_Light.png';
import Collection20 from './Collection-20.png';
@ -164,6 +166,8 @@ import SilverBox from './SilverBox.png';
import Spacedrop_Light from './Spacedrop_Light.png';
import Spacedrop1 from './Spacedrop-1.png';
import Spacedrop from './Spacedrop.png';
import Sync_Light from './Sync_Light.png';
import Sync from './Sync.png';
import Tablet_Light from './Tablet_Light.png';
import Tablet from './Tablet.png';
import Tags_Light from './Tags_Light.png';
@ -217,6 +221,8 @@ export {
BookBlue,
Book_Light,
Box,
CloudSync,
CloudSync_Light,
Code20,
Collection20,
Collection,
@ -353,6 +359,8 @@ export {
Spacedrop1,
Spacedrop,
Spacedrop_Light,
Sync,
Sync_Light,
Tablet,
Tablet_Light,
Tags,

View file

@ -0,0 +1,10 @@
/*
* This file was automatically generated by a script.
* To regenerate this file, run: pnpm assets gen
*/
import Fda from './Fda.mp4';
import SdIntro from './SdIntro.mp4';
import SdMobIntro from './SdMobIntro.mp4';
export { Fda, SdIntro, SdMobIntro };

View file

@ -383,9 +383,9 @@ export type JobProgressEvent = { id: string; library_id: string; task_count: num
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue }
export type KindStatistic = { kind: number; name: string; count: number; total_bytes: string }
export type KindStatistic = { kind: number; name: string; count: [number, number]; total_bytes: [number, number] }
export type KindStatistics = { statistics: KindStatistic[] }
export type KindStatistics = { statistics: KindStatistic[]; total_identified_files: number; total_unidentified_files: number }
export type Label = { id: number; name: string; date_created: string | null; date_modified: string | null }

View file

@ -86,6 +86,7 @@ export interface ByteSizeOpts {
precision?: number;
base_unit?: 'decimal' | 'binary';
use_plural?: boolean;
no_thousands?: boolean;
}
/**
@ -98,6 +99,7 @@ export interface ByteSizeOpts {
* @param options.precision - Number of decimal places. Defaults to `1`.
* @param options.base_unit - The base unit to use. Defaults to `'decimal'`.
* @param options.use_plural - Use plural unit names when necessary. Defaults to `true`.
* @param options.no_thousands - Do not convert TB to thousands. Defaults to `true`.
*/
export const humanizeSize = (
value: null | string | number | bigint | string[] | number[] | bigint[] | undefined,
@ -106,7 +108,8 @@ export const humanizeSize = (
precision = 1,
locales,
base_unit = 'decimal',
use_plural = true
use_plural = true,
no_thousands = true
}: ByteSizeOpts = {}
) => {
if (value == null) value = 0n;
@ -135,6 +138,13 @@ export const humanizeSize = (
: Number((bytes * BigInt(precisionFactor)) / unit.from) / precisionFactor;
const plural = use_plural && value !== 1 ? 's' : '';
//TODO: Improve this
// Convert to thousands when short is TB to show correct progress value
//i.e 2.5 TB = 2500
if (unit.short === "TB" && !no_thousands) {
value = value * 1000;
}
return {
unit: is_bit ? BYTE_TO_BIT[unit.short as keyof typeof BYTE_TO_BIT] : unit.short,
long: is_bit ? BYTE_TO_BIT[unit.long as keyof typeof BYTE_TO_BIT] : unit.long,

View file

@ -1,10 +1,10 @@
import clsx from 'clsx';
import { Puff } from 'react-loading-icons';
export function Loader(props: { className?: string }) {
export function Loader(props: { className?: string, color?: string}) {
return (
<Puff
stroke="#2599FF"
stroke={props.color || '#2599FF'}
strokeOpacity={4}
strokeWidth={5}
speed={1}

View file

@ -147,7 +147,7 @@ importers:
version: 2.16.0
'@tauri-apps/cli':
specifier: next
version: 2.0.0-beta.19
version: 2.0.0-beta.20
'@types/react':
specifier: ^18.2.67
version: 18.2.67
@ -760,6 +760,9 @@ importers:
crypto-random-string:
specifier: ^5.0.0
version: 5.0.0
d3-force:
specifier: ^3.0.0
version: 3.0.0
dayjs:
specifier: ^1.11.10
version: 1.11.10
@ -793,6 +796,9 @@ importers:
react-error-boundary:
specifier: ^4.0.11
version: 4.0.13(react@18.2.0)
react-force-graph-2d:
specifier: ^1.25.5
version: 1.25.5(react@18.2.0)
react-hook-form:
specifier: ^7.47.0
version: 7.51.1(react@18.2.0)
@ -863,6 +869,9 @@ importers:
'@sd/config':
specifier: workspace:*
version: link:../packages/config
'@types/d3-force':
specifier: ^3.0.9
version: 3.0.9
'@types/node':
specifier: '>18.18.x'
version: 20.11.29
@ -3560,9 +3569,6 @@ packages:
'@radix-ui/number@1.0.1':
resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
'@radix-ui/primitive@1.0.0':
resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==}
'@radix-ui/primitive@1.0.1':
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
@ -3605,11 +3611,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-compose-refs@1.0.0':
resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-compose-refs@1.0.1':
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
peerDependencies:
@ -3663,12 +3664,6 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.0.2':
resolution: {integrity: sha512-WjJzMrTWROozDqLB0uRWYvj4UuXsM/2L19EmQ3Au+IJWqwvwq9Bwd+P8ivo0Deg9JDPArR1I6MbWNi1CmXsskg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-dismissable-layer@1.0.4':
resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==}
peerDependencies:
@ -3843,12 +3838,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@1.0.1':
resolution: {integrity: sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-primitive@1.0.3':
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
peerDependencies:
@ -3927,11 +3916,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.0.1':
resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-slot@1.0.2':
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
@ -3993,11 +3977,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.0.0':
resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-use-callback-ref@1.0.1':
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
peerDependencies:
@ -4016,11 +3995,6 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.0.2':
resolution: {integrity: sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
'@radix-ui/react-use-escape-keydown@1.0.3':
resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
peerDependencies:
@ -4849,6 +4823,7 @@ packages:
'@storybook/testing-library@0.2.2':
resolution: {integrity: sha512-L8sXFJUHmrlyU2BsWWZGuAjv39Jl1uAqUHdxmN42JY15M4+XCMjGlArdCCjDe1wpTSW6USYISA9axjZojgtvnw==}
deprecated: In Storybook 8, this package functionality has been integrated to a new package called @storybook/test, which uses Vitest APIs for an improved experience. When upgrading to Storybook 8 with 'npx storybook@latest upgrade', you will get prompted and will get an automigration for the new package. Please migrate when you can.
'@storybook/theming@8.0.1':
resolution: {integrity: sha512-TUmSHRh3YrpJ25DYjD+9PpJaq9Qf9P1S2xpwfNARM9r2KpkMF1/RgqnnQgZpP9od0Tzvkji7XPzxPU//EmQKEA==}
@ -5118,68 +5093,68 @@ packages:
resolution: {integrity: sha512-Np1opKANzRMF3lgJ9gDquBCB9SxlE2lRmNpVx1+L6RyzAmigkuh0ZulT5jMnDA3JLsuSDU135r/s4t/Pmx4atg==}
engines: {node: '>= 18', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
'@tauri-apps/cli-darwin-arm64@2.0.0-beta.19':
resolution: {integrity: sha512-c4KvyBnQ5C/P3oAyO7WZ71xYxW8yMwDe3I4Ik3Uz6+AXZ2k3xPx19VuxCgTJdJCkxtLvhAGu9Q2IZQuuDoGTsg==}
'@tauri-apps/cli-darwin-arm64@2.0.0-beta.20':
resolution: {integrity: sha512-oCJOCib7GuYkwkBXx+ekamR8NZZU+2i3MLP+DHpDxK5gS2uhCE+CBkamJkNt6y1x6xdVnwyqZOm5RvN4SRtyIA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.0.0-beta.19':
resolution: {integrity: sha512-t7rzloJwzgNXm82/w97Tq3RcvX7XmRcaxnu8ujV5SrREFxzLNRpkyzzr/vVthV7FZjKGcQf5QmJ3XeGXUfkCfQ==}
'@tauri-apps/cli-darwin-x64@2.0.0-beta.20':
resolution: {integrity: sha512-lC5QSnRExedYN4Ds6ZlSvC2PxP8qfIYBJQ5ktf+PJI5gQALdNeVtd6YnTG1ODCEklfLq9WKkGwp7JdALTU5wDA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.19':
resolution: {integrity: sha512-ZnM596ltSUNeBKH9rMGm1Ch1lCaeb1rW79nP1E6REuu1iOBpVAdkporaMWE7JSpkBZmSZdSuVDRhrMDuG7Uc6A==}
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.20':
resolution: {integrity: sha512-nZCeBMHHye5DLOJV5k2w658hnCS+LYaOZ8y/G9l3ei+g0L/HBjlSy6r4simsAT5TG8+l3oCZzLBngfTMdDS/YA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.19':
resolution: {integrity: sha512-xHAFx+6EqEKLQMrqQPwnzhygA2b/nn0b7pLF48YBvkDj3KLOmv5cC+K34f2l0KIaLB8B/oVFAQKsfet4XLew+w==}
'@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.20':
resolution: {integrity: sha512-B79ISVLPVBgwnCchVqwTKU+vxnFYqxKomcR4rmsvxfs0NVtT5QuNzE1k4NUQnw3966yjwhYR3mnHsSJQSB4Eyw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.19':
resolution: {integrity: sha512-ySRYhIfNDt/VXCycVt7d/dMBXf7L9iWf0SwynZ2nvJU/MaHIfJUgV68/l3RTRooYOCkYN4v/RRcGFD3wRmtE5g==}
'@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.20':
resolution: {integrity: sha512-ojIkv/1uZHhcrgfIN8xgn4BBeo/Xg+bnV0wer6lD78zyxkUMWeEZ+u3mae1ejCJNhhaZOxNaUQ67MvDOiGyr5Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.19':
resolution: {integrity: sha512-GEySXBulHQfGr3xuuv2ShnUrQtrWn3ynUtftoMiJNlpa1RTLfzglbUdA7zXag65E8h2jATVYnC/n8/sE5jtSHw==}
'@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.20':
resolution: {integrity: sha512-xBy1FNbHKlc7T6pOmFQQPECxJaI5A9QWX7Kb9N64cNVusoOGlvc3xHYkXMS4PTr7xXOT0yiE1Ww2OwDRJ3lYsg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tauri-apps/cli-linux-x64-musl@2.0.0-beta.19':
resolution: {integrity: sha512-gz1x/7EhpMcIhUvR7RhG3D+dwUnXF+MIxPoiuDAKzQAj3i6qacZJvwxyRcpVQ6HaUDpmtaHz0AKpWIMmIFL90g==}
'@tauri-apps/cli-linux-x64-musl@2.0.0-beta.20':
resolution: {integrity: sha512-+O6zq5jmtUxA1FUAAwF2ywPysy4NRo2Y6G+ESZDkY9XosRwdt5OUjqAsYktZA3AxDMZVei8r9buwTqUwi9ny/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.19':
resolution: {integrity: sha512-Zz/UwU+7QQbz9lu9cpLzX/fCgmBG1lX+K5O97kTJVcqgBiS0zUc5q1efYr7ex4c6NLVP7uaUK3IKwctBy2MvEA==}
'@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.20':
resolution: {integrity: sha512-RswgMbWyOQcv53CHvIuiuhAh4kKDqaGyZfWD4VlxqX/XhkoF5gsNgr0MxzrY7pmoL+89oVI+fiGVJz4nOQE5vA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.19':
resolution: {integrity: sha512-fdT/u8I31PryeqULgzzUV+bYAlgt9WStJaZWt1/hMDffB9VViL3gO7V67mtNUEhBUMaX/SqItwklbJyy3TKXXg==}
'@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.20':
resolution: {integrity: sha512-5lgWmDVXhX3SBGbiv5SduM1yajiRnUEJClWhSdRrEEJeXdsxpCsBEhxYnUnDCEzPKxLLn5fdBv3VrVctJ03csQ==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.19':
resolution: {integrity: sha512-EHTi4D95mTmPC/MqWU5mBGhwZ0i82iVKEAAGaKDNdwYzibmioeANCzsD8eeyuU0kCE5BCWBYpA+2epGQnfDjMg==}
'@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.20':
resolution: {integrity: sha512-SuSiiVQTQPSzWlsxQp/NMzWbzDS9TdVDOw7CCfgiG5wnT2GsxzrcIAVN6i7ILsVFLxrjr0bIgPldSJcdcH84Yw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.0.0-beta.19':
resolution: {integrity: sha512-IHbgyUpnXY5ZEenQUz2Gce7w1Xl1BgLR6Jyf6SN0VbUVr9qJdSRPN7/FK+4JQFt2DC9076NVYTQFLOt03KNbwA==}
'@tauri-apps/cli@2.0.0-beta.20':
resolution: {integrity: sha512-707q9uIc2oNrYHd2dtMvxTrpZXVpart5EIktnRymNOpphkLlB6WUBjHD+ga45WqTU6cNGKbYvkKqTNfshNul9Q==}
engines: {node: '>= 10'}
hasBin: true
@ -5403,6 +5378,9 @@ packages:
'@types/cross-spawn@6.0.6':
resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==}
'@types/d3-force@3.0.9':
resolution: {integrity: sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@ -5866,6 +5844,10 @@ packages:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
accessor-fn@1.5.0:
resolution: {integrity: sha512-dml7D96DY/K5lt4Ra2jMnpL9Bhw5HEGws4p1OAIxFFj9Utd/RxNfEO3T3f0QIWFNwQU7gNxH9snUfqF/zNkP/w==}
engines: {node: '>=12'}
acorn-import-assertions@1.9.0:
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
peerDependencies:
@ -6023,6 +6005,7 @@ packages:
are-we-there-yet@1.1.7:
resolution: {integrity: sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==}
deprecated: This package is no longer supported.
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@ -6275,6 +6258,9 @@ packages:
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
engines: {node: '>=12.0.0'}
bezier-js@6.1.4:
resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==}
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
@ -6499,6 +6485,10 @@ packages:
caniuse-lite@1.0.30001599:
resolution: {integrity: sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==}
canvas-color-tracker@1.2.1:
resolution: {integrity: sha512-i5clg2pEdaWqHuEM/B74NZNLkHh5+OkXbA/T4iaBiaNDagkOCXkLNrhqUfdUugsRwuaNRU20e/OygzxWRor3yg==}
engines: {node: '>=12'}
caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
@ -7019,6 +7009,86 @@ packages:
engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
hasBin: true
d3-array@3.2.4:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'}
d3-binarytree@1.0.2:
resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==}
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-force-3d@3.0.5:
resolution: {integrity: sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==}
engines: {node: '>=12'}
d3-force@3.0.0:
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
engines: {node: '>=12'}
d3-format@3.1.0:
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-octree@1.0.2:
resolution: {integrity: sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA==}
d3-quadtree@3.0.1:
resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
engines: {node: '>=12'}
d3-scale-chromatic@3.1.0:
resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
engines: {node: '>=12'}
d3-scale@4.0.2:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
d3-time-format@4.1.0:
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
engines: {node: '>=12'}
d3-time@3.1.0:
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
engines: {node: '>=12'}
d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
d3-transition@3.0.1:
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
dag-map@1.0.2:
resolution: {integrity: sha512-+LSAiGFwQ9dRnRdOeaj7g47ZFJcOUPukAP8J3A3fuZ1g9Y44BG+P1sgApjLXTQPOzC4+7S9Wr8kXsfpINM4jpw==}
@ -8100,6 +8170,10 @@ packages:
for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
force-graph@1.43.5:
resolution: {integrity: sha512-HveLELh9yhZXO/QOfaFS38vlwJZ/3sKu+jarfXzRmbmihSOH/BbRWnUvmg8wLFiYy6h4HlH4lkRfZRccHYmXgA==}
engines: {node: '>=12'}
foreach@2.0.6:
resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==}
@ -8171,6 +8245,9 @@ packages:
from@0.1.7:
resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
fromentries@1.3.2:
resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==}
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@ -8233,6 +8310,7 @@ packages:
gauge@2.7.4:
resolution: {integrity: sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==}
deprecated: This package is no longer supported.
gensequence@7.0.0:
resolution: {integrity: sha512-47Frx13aZh01afHJTB3zTtKIlFI6vWY+MYCN9Qpew6i52rfKjnhCF/l1YlC8UmEMvvntZZ6z4PiCcmyuedR2aQ==}
@ -8728,6 +8806,10 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
index-array-by@1.4.1:
resolution: {integrity: sha512-Zu6THdrxQdyTuT2uA5FjUoBEsFHPzHcPIj18FszN6yXKHxSfGcR4TPLabfuT//E25q1Igyx9xta2WMvD/x9P/g==}
engines: {node: '>=12'}
indexof@0.0.1:
resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==}
@ -8740,6 +8822,7 @@ packages:
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@ -8769,6 +8852,10 @@ packages:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'}
internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
interpret@1.4.0:
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
engines: {node: '>= 0.10'}
@ -9124,6 +9211,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
jerrypick@1.1.1:
resolution: {integrity: sha512-XTtedPYEyVp4t6hJrXuRKr/jHj8SC4z+4K0b396PMkov6muL+i8IIamJIvZWe3jUspgIJak0P+BaWKawMYNBLg==}
engines: {node: '>=12'}
jest-environment-node@29.7.0:
resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -9274,6 +9365,10 @@ packages:
jwt-decode@3.1.2:
resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==}
kapsule@1.14.5:
resolution: {integrity: sha512-H0iSpTynUzZw3tgraDmReprpFRmH5oP5GPmaNsurSwLx2H5iCpOMIkp5q+sfhB4Tz/UJd1E1IbEE9Z6ksnJ6RA==}
engines: {node: '>=12'}
katex@0.16.9:
resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==}
hasBin: true
@ -9462,6 +9557,9 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
@ -10353,6 +10451,7 @@ packages:
npmlog@4.1.2:
resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==}
deprecated: This package is no longer supported.
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
@ -10508,6 +10607,7 @@ packages:
osenv@0.1.5:
resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==}
deprecated: This package is no longer supported.
ospath@1.2.2:
resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
@ -11145,6 +11245,12 @@ packages:
peerDependencies:
react: '>=16.13.1'
react-force-graph-2d@1.25.5:
resolution: {integrity: sha512-3u8WjZZorpwZSDs3n3QeOS9ZoxFPM+IR9SStYJVQ/qKECydMHarxnf7ynV/MKJbC6kUsc60soD0V+Uq/r2vz7Q==}
engines: {node: '>=12'}
peerDependencies:
react: '*'
react-freeze@1.0.4:
resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==}
engines: {node: '>=10'}
@ -11208,6 +11314,12 @@ packages:
react: ^17.0.0 || ^16.3.0 || ^15.5.4
react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4
react-kapsule@2.4.0:
resolution: {integrity: sha512-w4Yv9CgWdj8kWGQEPNWFGJJ08dYEZHZpiaFR/DgZjCMBNqv9wus2Gy1qvHVJmJbzvAZbq6jdvFC+NYzEqAlNhQ==}
engines: {node: '>=12'}
peerDependencies:
react: '>=16.13.1'
react-lifecycles-compat@3.0.4:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
@ -11715,18 +11827,22 @@ packages:
rimraf@2.4.5:
resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@2.6.3:
resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
ripemd160@2.0.2:
@ -12475,6 +12591,9 @@ packages:
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
tinycolor2@1.6.0:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
tinyspy@2.2.1:
resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==}
engines: {node: '>=14.0.0'}
@ -16333,10 +16452,6 @@ snapshots:
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/primitive@1.0.0':
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/primitive@1.0.1':
dependencies:
'@babel/runtime': 7.24.0
@ -16381,11 +16496,6 @@ snapshots:
'@types/react': 18.2.67
'@types/react-dom': 18.2.22
'@radix-ui/react-compose-refs@1.0.0(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
react: 18.2.0
'@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.67)(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -16445,17 +16555,6 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.67
'@radix-ui/react-dismissable-layer@1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
'@radix-ui/react-use-escape-keydown': 1.0.2(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
'@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -16547,7 +16646,7 @@ snapshots:
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.67)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.67)(react@18.2.0)
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.67)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.67)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.67)(react@18.2.0)
@ -16659,13 +16758,6 @@ snapshots:
'@types/react': 18.2.67
'@types/react-dom': 18.2.22
'@radix-ui/react-primitive@1.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/react-slot': 1.0.1(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
'@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -16774,12 +16866,6 @@ snapshots:
'@types/react': 18.2.67
'@types/react-dom': 18.2.22
'@radix-ui/react-slot@1.0.1(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
react: 18.2.0
'@radix-ui/react-slot@1.0.2(@types/react@18.2.67)(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -16863,11 +16949,6 @@ snapshots:
'@types/react': 18.2.67
'@types/react-dom': 18.2.22
'@radix-ui/react-use-callback-ref@1.0.0(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
react: 18.2.0
'@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.67)(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -16883,12 +16964,6 @@ snapshots:
optionalDependencies:
'@types/react': 18.2.67
'@radix-ui/react-use-escape-keydown@1.0.2(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
react: 18.2.0
'@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.67)(react@18.2.0)':
dependencies:
'@babel/runtime': 7.24.0
@ -18783,48 +18858,48 @@ snapshots:
'@tauri-apps/api@2.0.0-beta.13': {}
'@tauri-apps/cli-darwin-arm64@2.0.0-beta.19':
'@tauri-apps/cli-darwin-arm64@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-darwin-x64@2.0.0-beta.19':
'@tauri-apps/cli-darwin-x64@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.19':
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.19':
'@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.19':
'@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.19':
'@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.0.0-beta.19':
'@tauri-apps/cli-linux-x64-musl@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.19':
'@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.19':
'@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.20':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.19':
'@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.20':
optional: true
'@tauri-apps/cli@2.0.0-beta.19':
'@tauri-apps/cli@2.0.0-beta.20':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.0.0-beta.19
'@tauri-apps/cli-darwin-x64': 2.0.0-beta.19
'@tauri-apps/cli-linux-arm-gnueabihf': 2.0.0-beta.19
'@tauri-apps/cli-linux-arm64-gnu': 2.0.0-beta.19
'@tauri-apps/cli-linux-arm64-musl': 2.0.0-beta.19
'@tauri-apps/cli-linux-x64-gnu': 2.0.0-beta.19
'@tauri-apps/cli-linux-x64-musl': 2.0.0-beta.19
'@tauri-apps/cli-win32-arm64-msvc': 2.0.0-beta.19
'@tauri-apps/cli-win32-ia32-msvc': 2.0.0-beta.19
'@tauri-apps/cli-win32-x64-msvc': 2.0.0-beta.19
'@tauri-apps/cli-darwin-arm64': 2.0.0-beta.20
'@tauri-apps/cli-darwin-x64': 2.0.0-beta.20
'@tauri-apps/cli-linux-arm-gnueabihf': 2.0.0-beta.20
'@tauri-apps/cli-linux-arm64-gnu': 2.0.0-beta.20
'@tauri-apps/cli-linux-arm64-musl': 2.0.0-beta.20
'@tauri-apps/cli-linux-x64-gnu': 2.0.0-beta.20
'@tauri-apps/cli-linux-x64-musl': 2.0.0-beta.20
'@tauri-apps/cli-win32-arm64-msvc': 2.0.0-beta.20
'@tauri-apps/cli-win32-ia32-msvc': 2.0.0-beta.20
'@tauri-apps/cli-win32-x64-msvc': 2.0.0-beta.20
'@tauri-apps/plugin-dialog@2.0.0-beta.3':
dependencies:
@ -19125,6 +19200,8 @@ snapshots:
dependencies:
'@types/node': 20.11.29
'@types/d3-force@3.0.9': {}
'@types/debug@4.1.12':
dependencies:
'@types/ms': 0.7.34
@ -19679,6 +19756,8 @@ snapshots:
mime-types: 2.1.35
negotiator: 0.6.3
accessor-fn@1.5.0: {}
acorn-import-assertions@1.9.0(acorn@8.11.3):
dependencies:
acorn: 8.11.3
@ -20181,6 +20260,8 @@ snapshots:
dependencies:
open: 8.4.2
bezier-js@6.1.4: {}
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
@ -20452,6 +20533,10 @@ snapshots:
caniuse-lite@1.0.30001599: {}
canvas-color-tracker@1.2.1:
dependencies:
tinycolor2: 1.6.0
caseless@0.12.0: {}
ccount@2.0.1: {}
@ -21110,6 +21195,89 @@ snapshots:
untildify: 4.0.0
yauzl: 2.10.0
d3-array@3.2.4:
dependencies:
internmap: 2.0.3
d3-binarytree@1.0.2: {}
d3-color@3.1.0: {}
d3-dispatch@3.0.1: {}
d3-drag@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
d3-ease@3.0.1: {}
d3-force-3d@3.0.5:
dependencies:
d3-binarytree: 1.0.2
d3-dispatch: 3.0.1
d3-octree: 1.0.2
d3-quadtree: 3.0.1
d3-timer: 3.0.1
d3-force@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-quadtree: 3.0.1
d3-timer: 3.0.1
d3-format@3.1.0: {}
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-octree@1.0.2: {}
d3-quadtree@3.0.1: {}
d3-scale-chromatic@3.1.0:
dependencies:
d3-color: 3.1.0
d3-interpolate: 3.0.1
d3-scale@4.0.2:
dependencies:
d3-array: 3.2.4
d3-format: 3.1.0
d3-interpolate: 3.0.1
d3-time: 3.1.0
d3-time-format: 4.1.0
d3-selection@3.0.0: {}
d3-time-format@4.1.0:
dependencies:
d3-time: 3.1.0
d3-time@3.1.0:
dependencies:
d3-array: 3.2.4
d3-timer@3.0.1: {}
d3-transition@3.0.1(d3-selection@3.0.0):
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
d3-zoom@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
dag-map@1.0.2: {}
damerau-levenshtein@1.0.8: {}
@ -22637,6 +22805,23 @@ snapshots:
dependencies:
is-callable: 1.2.7
force-graph@1.43.5:
dependencies:
'@tweenjs/tween.js': 23.1.1
accessor-fn: 1.5.0
bezier-js: 6.1.4
canvas-color-tracker: 1.2.1
d3-array: 3.2.4
d3-drag: 3.0.0
d3-force-3d: 3.0.5
d3-scale: 4.0.2
d3-scale-chromatic: 3.1.0
d3-selection: 3.0.0
d3-zoom: 3.0.0
index-array-by: 1.4.1
kapsule: 1.14.5
lodash-es: 4.17.21
foreach@2.0.6: {}
foreground-child@3.1.1:
@ -22707,6 +22892,8 @@ snapshots:
from@0.1.7: {}
fromentries@1.3.2: {}
fs-constants@1.0.0: {}
fs-extra@10.1.0:
@ -23405,6 +23592,8 @@ snapshots:
indent-string@4.0.0: {}
index-array-by@1.4.1: {}
indexof@0.0.1: {}
infer-owner@1.0.4: {}
@ -23439,6 +23628,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.0.6
internmap@2.0.3: {}
interpret@1.4.0: {}
invariant@2.2.4:
@ -23725,6 +23916,8 @@ snapshots:
filelist: 1.0.4
minimatch: 3.1.2
jerrypick@1.1.1: {}
jest-environment-node@29.7.0:
dependencies:
'@jest/environment': 29.7.0
@ -23935,6 +24128,10 @@ snapshots:
jwt-decode@3.1.2: {}
kapsule@1.14.5:
dependencies:
lodash-es: 4.17.21
katex@0.16.9:
dependencies:
commander: 8.3.0
@ -24138,6 +24335,8 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash-es@4.17.21: {}
lodash.camelcase@4.3.0: {}
lodash.castarray@4.4.0: {}
@ -26358,6 +26557,13 @@ snapshots:
'@babel/runtime': 7.24.0
react: 18.2.0
react-force-graph-2d@1.25.5(react@18.2.0):
dependencies:
force-graph: 1.43.5
prop-types: 15.8.1
react: 18.2.0
react-kapsule: 2.4.0(react@18.2.0)
react-freeze@1.0.4(react@18.2.0):
dependencies:
react: 18.2.0
@ -26412,6 +26618,12 @@ snapshots:
- '@types/react'
- encoding
react-kapsule@2.4.0(react@18.2.0):
dependencies:
fromentries: 1.3.2
jerrypick: 1.1.1
react: 18.2.0
react-lifecycles-compat@3.0.4: {}
react-loading-icons@1.1.0: {}
@ -28120,6 +28332,8 @@ snapshots:
tiny-invariant@1.3.3: {}
tinycolor2@1.6.0: {}
tinyspy@2.2.1: {}
tmp@0.0.33: