This commit is contained in:
Jamie Pine committed 2022-05-10 09:03:20 -07:00
1 parent 99938128bc
commit 81fa5f8a2f
32 files changed
+723 -430

No files matched your search

+1
View File
@@ -2,6 +2,7 @@
"cSpell.words": [
"actix",
"bpfrpt",
"consts",
"creationdate",
"ipfs",
"Keepsafe",
+10 -12
View File
@@ -1,7 +1,7 @@
use std::time::{Duration, Instant};
use std::env::consts;
use std::time::{Duration, Instant};
use sdcore::{ClientCommand, ClientQuery, Core, CoreController, CoreEvent, CoreResponse};
use sdcore::{ClientCommand, ClientQuery, CoreController, CoreEvent, CoreResponse, Node};
use tauri::api::path;
use tauri::Manager;
mod menu;
@@ -38,14 +38,14 @@ async fn client_command_transport(
async fn main() {
let data_dir = path::data_dir().unwrap_or(std::path::PathBuf::from("./"));
// create an instance of the core
let (mut core, mut event_receiver) = Core::new(data_dir).await;
let (mut node, mut event_receiver) = Node::new(data_dir).await;
// run startup tasks
core.initializer().await;
// extract the core controller
let controller = core.get_controller();
// throw the core into a dedicated thread
node.initializer().await;
// extract the node controller
let controller = node.get_controller();
// throw the node into a dedicated thread
tokio::spawn(async move {
core.start().await;
node.start().await;
});
// create tauri app
tauri::Builder::default()
@@ -60,16 +60,14 @@ async fn main() {
window_shadows::set_shadow(&window, true).unwrap_or(());
if consts::OS == "windows" {
window.set_decorations(true);
println!("Hello World!");
window.set_decorations(true).unwrap_or(());
println!("Hello World!");
}
window.start_dragging().unwrap_or(());
});
}
// core event transport
tokio::spawn(async move {
let mut last = Instant::now();
+5 -5
View File
@@ -1,4 +1,4 @@
use sdcore::{ClientCommand, ClientQuery, Core, CoreController, CoreEvent, CoreResponse};
use sdcore::{ClientCommand, ClientQuery, CoreController, CoreEvent, CoreResponse, Node};
use std::{env, path::Path};
use actix::{
@@ -196,14 +196,14 @@ async fn setup() -> (
},
};
let (mut core, event_receiver) = Core::new(data_dir_path).await;
let (mut node, event_receiver) = Node::new(data_dir_path).await;
core.initializer().await;
node.initializer().await;
let controller = core.get_controller();
let controller = node.get_controller();
tokio::spawn(async move {
core.start().await;
node.start().await;
});
(web::Data::new(event_receiver), web::Data::new(controller))
@@ -0,0 +1,177 @@
/*
Warnings:
- You are about to drop the `clients` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the column `client_id` on the `sync_events` table. All the data in the column will be lost.
- You are about to drop the column `client_id` on the `locations` table. All the data in the column will be lost.
- You are about to drop the column `client_id` on the `jobs` table. All the data in the column will be lost.
- You are about to drop the column `encryption` on the `tags` table. All the data in the column will be lost.
- You are about to drop the column `client_id` on the `volumes` table. All the data in the column will be lost.
- Added the required column `node_id` to the `sync_events` table without a default value. This is not possible if the table is not empty.
- Added the required column `node_id` to the `jobs` table without a default value. This is not possible if the table is not empty.
- Added the required column `node_id` to the `volumes` table without a default value. This is not possible if the table is not empty.
*/
-- DropIndex
DROP INDEX "clients_pub_id_key";
-- DropTable
PRAGMA foreign_keys=off;
DROP TABLE "clients";
PRAGMA foreign_keys=on;
-- CreateTable
CREATE TABLE "nodes" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"platform" INTEGER NOT NULL DEFAULT 0,
"version" TEXT,
"online" BOOLEAN DEFAULT true,
"last_seen" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"timezone" TEXT,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "keys" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"checksum" TEXT NOT NULL,
"name" TEXT,
"date_created" DATETIME DEFAULT CURRENT_TIMESTAMP,
"algorithm" INTEGER DEFAULT 0
);
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_files" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"cas_id" TEXT NOT NULL,
"integrity_checksum" TEXT,
"kind" INTEGER NOT NULL DEFAULT 0,
"size_in_bytes" TEXT NOT NULL,
"encryption" INTEGER NOT NULL DEFAULT 0,
"key_id" INTEGER,
"hidden" BOOLEAN NOT NULL DEFAULT false,
"favorite" BOOLEAN NOT NULL DEFAULT false,
"important" BOOLEAN NOT NULL DEFAULT false,
"has_thumbnail" BOOLEAN NOT NULL DEFAULT false,
"has_thumbstrip" BOOLEAN NOT NULL DEFAULT false,
"has_video_preview" BOOLEAN NOT NULL DEFAULT false,
"ipfs_id" TEXT,
"comment" TEXT,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "files_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "keys" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_files" ("cas_id", "comment", "date_created", "date_indexed", "date_modified", "encryption", "favorite", "has_thumbnail", "has_thumbstrip", "has_video_preview", "hidden", "id", "important", "integrity_checksum", "ipfs_id", "kind", "size_in_bytes") SELECT "cas_id", "comment", "date_created", "date_indexed", "date_modified", "encryption", "favorite", "has_thumbnail", "has_thumbstrip", "has_video_preview", "hidden", "id", "important", "integrity_checksum", "ipfs_id", "kind", "size_in_bytes" FROM "files";
DROP TABLE "files";
ALTER TABLE "new_files" RENAME TO "files";
CREATE UNIQUE INDEX "files_cas_id_key" ON "files"("cas_id");
CREATE UNIQUE INDEX "files_integrity_checksum_key" ON "files"("integrity_checksum");
CREATE TABLE "new_sync_events" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"node_id" INTEGER NOT NULL,
"timestamp" TEXT NOT NULL,
"data" TEXT NOT NULL,
CONSTRAINT "sync_events_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_sync_events" ("data", "id", "timestamp") SELECT "data", "id", "timestamp" FROM "sync_events";
DROP TABLE "sync_events";
ALTER TABLE "new_sync_events" RENAME TO "sync_events";
CREATE TABLE "new_locations" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" TEXT NOT NULL,
"node_id" INTEGER,
"name" TEXT,
"local_path" TEXT,
"total_capacity" INTEGER,
"available_capacity" INTEGER,
"filesystem" TEXT,
"disk_type" INTEGER,
"is_removable" BOOLEAN,
"is_online" BOOLEAN NOT NULL DEFAULT true,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "new_locations" ("available_capacity", "date_created", "disk_type", "filesystem", "id", "is_online", "is_removable", "local_path", "name", "pub_id", "total_capacity") SELECT "available_capacity", "date_created", "disk_type", "filesystem", "id", "is_online", "is_removable", "local_path", "name", "pub_id", "total_capacity" FROM "locations";
DROP TABLE "locations";
ALTER TABLE "new_locations" RENAME TO "locations";
CREATE UNIQUE INDEX "locations_pub_id_key" ON "locations"("pub_id");
CREATE TABLE "new_jobs" (
"id" TEXT NOT NULL PRIMARY KEY,
"node_id" INTEGER NOT NULL,
"action" INTEGER NOT NULL,
"status" INTEGER NOT NULL DEFAULT 0,
"task_count" INTEGER NOT NULL DEFAULT 1,
"completed_task_count" INTEGER NOT NULL DEFAULT 0,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"seconds_elapsed" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "jobs_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_jobs" ("action", "completed_task_count", "date_created", "date_modified", "id", "seconds_elapsed", "status", "task_count") SELECT "action", "completed_task_count", "date_created", "date_modified", "id", "seconds_elapsed", "status", "task_count" FROM "jobs";
DROP TABLE "jobs";
ALTER TABLE "new_jobs" RENAME TO "jobs";
CREATE TABLE "new_tags" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" TEXT NOT NULL,
"name" TEXT,
"total_files" INTEGER DEFAULT 0,
"redundancy_goal" INTEGER DEFAULT 1,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "new_tags" ("date_created", "date_modified", "id", "name", "pub_id", "redundancy_goal", "total_files") SELECT "date_created", "date_modified", "id", "name", "pub_id", "redundancy_goal", "total_files" FROM "tags";
DROP TABLE "tags";
ALTER TABLE "new_tags" RENAME TO "tags";
CREATE UNIQUE INDEX "tags_pub_id_key" ON "tags"("pub_id");
CREATE TABLE "new_file_paths" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"is_dir" BOOLEAN NOT NULL DEFAULT false,
"location_id" INTEGER NOT NULL,
"materialized_path" TEXT NOT NULL,
"name" TEXT NOT NULL,
"extension" TEXT,
"file_id" INTEGER,
"parent_id" INTEGER,
"encryption" INTEGER NOT NULL DEFAULT 0,
"key_id" INTEGER,
"permissions" TEXT,
"temp_cas_id" TEXT,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "file_paths_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "locations" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "file_paths_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "file_paths_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "file_paths" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "file_paths_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "keys" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_file_paths" ("date_created", "date_indexed", "date_modified", "encryption", "extension", "file_id", "id", "is_dir", "location_id", "materialized_path", "name", "parent_id", "permissions", "temp_cas_id") SELECT "date_created", "date_indexed", "date_modified", "encryption", "extension", "file_id", "id", "is_dir", "location_id", "materialized_path", "name", "parent_id", "permissions", "temp_cas_id" FROM "file_paths";
DROP TABLE "file_paths";
ALTER TABLE "new_file_paths" RENAME TO "file_paths";
CREATE UNIQUE INDEX "file_paths_location_id_materialized_path_name_extension_key" ON "file_paths"("location_id", "materialized_path", "name", "extension");
CREATE TABLE "new_volumes" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"node_id" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"mount_point" TEXT NOT NULL,
"total_bytes_capacity" TEXT NOT NULL DEFAULT '0',
"total_bytes_available" TEXT NOT NULL DEFAULT '0',
"disk_type" TEXT,
"filesystem" TEXT,
"is_system" BOOLEAN NOT NULL DEFAULT false,
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "new_volumes" ("date_modified", "disk_type", "filesystem", "id", "is_system", "mount_point", "name", "total_bytes_available", "total_bytes_capacity") SELECT "date_modified", "disk_type", "filesystem", "id", "is_system", "mount_point", "name", "total_bytes_available", "total_bytes_capacity" FROM "volumes";
DROP TABLE "volumes";
ALTER TABLE "new_volumes" RENAME TO "volumes";
CREATE UNIQUE INDEX "volumes_node_id_mount_point_name_key" ON "volumes"("node_id", "mount_point", "name");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
-- CreateIndex
CREATE UNIQUE INDEX "nodes_pub_id_key" ON "nodes"("pub_id");
-- CreateIndex
CREATE UNIQUE INDEX "keys_checksum_key" ON "keys"("checksum");
@@ -0,0 +1,83 @@
/*
Warnings:
- You are about to drop the `spaces` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the column `encryption` on the `libraries` table. All the data in the column will be lost.
- You are about to drop the column `encryption` on the `files` table. All the data in the column will be lost.
- You are about to drop the column `encryption` on the `file_paths` table. All the data in the column will be lost.
- You are about to drop the column `permissions` on the `file_paths` table. All the data in the column will be lost.
- You are about to drop the column `temp_cas_id` on the `file_paths` table. All the data in the column will be lost.
*/
-- DropIndex
DROP INDEX "spaces_pub_id_key";
-- DropTable
PRAGMA foreign_keys=off;
DROP TABLE "spaces";
PRAGMA foreign_keys=on;
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_libraries" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"remote_id" TEXT,
"is_primary" BOOLEAN NOT NULL DEFAULT true,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"timezone" TEXT
);
INSERT INTO "new_libraries" ("date_created", "id", "is_primary", "name", "pub_id", "remote_id", "timezone") SELECT "date_created", "id", "is_primary", "name", "pub_id", "remote_id", "timezone" FROM "libraries";
DROP TABLE "libraries";
ALTER TABLE "new_libraries" RENAME TO "libraries";
CREATE UNIQUE INDEX "libraries_pub_id_key" ON "libraries"("pub_id");
CREATE TABLE "new_files" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"cas_id" TEXT NOT NULL,
"integrity_checksum" TEXT,
"kind" INTEGER NOT NULL DEFAULT 0,
"size_in_bytes" TEXT NOT NULL,
"key_id" INTEGER,
"hidden" BOOLEAN NOT NULL DEFAULT false,
"favorite" BOOLEAN NOT NULL DEFAULT false,
"important" BOOLEAN NOT NULL DEFAULT false,
"has_thumbnail" BOOLEAN NOT NULL DEFAULT false,
"has_thumbstrip" BOOLEAN NOT NULL DEFAULT false,
"has_video_preview" BOOLEAN NOT NULL DEFAULT false,
"ipfs_id" TEXT,
"comment" TEXT,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "files_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "keys" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_files" ("cas_id", "comment", "date_created", "date_indexed", "date_modified", "favorite", "has_thumbnail", "has_thumbstrip", "has_video_preview", "hidden", "id", "important", "integrity_checksum", "ipfs_id", "key_id", "kind", "size_in_bytes") SELECT "cas_id", "comment", "date_created", "date_indexed", "date_modified", "favorite", "has_thumbnail", "has_thumbstrip", "has_video_preview", "hidden", "id", "important", "integrity_checksum", "ipfs_id", "key_id", "kind", "size_in_bytes" FROM "files";
DROP TABLE "files";
ALTER TABLE "new_files" RENAME TO "files";
CREATE UNIQUE INDEX "files_cas_id_key" ON "files"("cas_id");
CREATE UNIQUE INDEX "files_integrity_checksum_key" ON "files"("integrity_checksum");
CREATE TABLE "new_file_paths" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"is_dir" BOOLEAN NOT NULL DEFAULT false,
"location_id" INTEGER NOT NULL,
"materialized_path" TEXT NOT NULL,
"name" TEXT NOT NULL,
"extension" TEXT,
"file_id" INTEGER,
"parent_id" INTEGER,
"key_id" INTEGER,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "file_paths_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "locations" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "file_paths_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "file_paths_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "file_paths" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "file_paths_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "keys" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_file_paths" ("date_created", "date_indexed", "date_modified", "extension", "file_id", "id", "is_dir", "key_id", "location_id", "materialized_path", "name", "parent_id") SELECT "date_created", "date_indexed", "date_modified", "extension", "file_id", "id", "is_dir", "key_id", "location_id", "materialized_path", "name", "parent_id" FROM "file_paths";
DROP TABLE "file_paths";
ALTER TABLE "new_file_paths" RENAME TO "file_paths";
CREATE UNIQUE INDEX "file_paths_location_id_materialized_path_name_extension_key" ON "file_paths"("location_id", "materialized_path", "name", "extension");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
+38 -34
View File
@@ -20,10 +20,10 @@ model Migration {
model SyncEvent {
id Int @id @default(autoincrement())
client_id Int
node_id Int
timestamp String
data String
client Client @relation(fields: [client_id], references: [id])
node Node @relation(fields: [node_id], references: [id])
@@map("sync_events")
}
@@ -34,10 +34,8 @@ model Library {
name String
remote_id String?
is_primary Boolean @default(true)
encryption Int @default(0)
date_created DateTime @default(now())
timezone String?
spaces Space[]
@@map("libraries")
}
@@ -57,7 +55,7 @@ model LibraryStatistics {
@@map("library_statistics")
}
model Client {
model Node {
id Int @id @default(autoincrement())
pub_id String @unique
name String
@@ -71,12 +69,12 @@ model Client {
sync_events SyncEvent[]
jobs Job[]
@@map("clients")
@@map("nodes")
}
model Volume {
id Int @id() @default(autoincrement())
client_id Int
node_id Int
name String
mount_point String
total_bytes_capacity String @default("0")
@@ -86,14 +84,14 @@ model Volume {
is_system Boolean @default(false)
date_modified DateTime @default(now())
@@unique([client_id, mount_point, name])
@@unique([node_id, mount_point, name])
@@map("volumes")
}
model Location {
id Int @id @default(autoincrement())
pub_id String @unique
client_id Int?
node_id Int?
name String?
local_path String?
total_capacity Int?
@@ -118,8 +116,7 @@ model File {
// basic metadata
kind Int @default(0)
size_in_bytes String
// mark uniqely as encrypted, will lead to all file paths being encrypted
encryption Int @default(0)
key_id Int?
// handy ways to mark a file
hidden Boolean @default(false)
favorite Boolean @default(false)
@@ -146,6 +143,8 @@ model File {
comments Comment[]
media_data MediaData?
key Key? @relation(fields: [key_id], references: [id])
@@map("files")
}
@@ -161,11 +160,11 @@ model FilePath {
extension String?
// the unique File for this file path
file_id Int?
//
// the parent in the file tree
parent_id Int?
encryption Int @default(0)
permissions String?
temp_cas_id String? // so a filepath can be created without its File, as they're created lazily
key_id Int? // replacement for encryption
// permissions String?
// temp_cas_id String? // so a filepath can be created without its File, as they're created lazily
date_created DateTime @default(now())
date_modified DateTime @default(now())
@@ -176,6 +175,8 @@ model FilePath {
parent FilePath? @relation("directory_file_paths", fields: [parent_id], references: [id])
children FilePath[] @relation("directory_file_paths")
key Key? @relation(fields: [key_id], references: [id])
@@unique([location_id, materialized_path, name, extension])
@@map("file_paths")
}
@@ -188,6 +189,23 @@ model FileConflict {
@@map("file_conflicts")
}
// keys allow us to know exactly which files can be decrypted with a given key
// they can be "mounted" to a client, and then used to decrypt files automatically
model Key {
id Int @id @default(autoincrement())
// used to identify the key when it is entered by user
checksum String @unique
name String?
// nullable if concealed for security
date_created DateTime? @default(now())
// so we know which algorithm to use, can be null if user must select
algorithm Int? @default(0)
files File[]
file_paths FilePath[]
@@map("keys")
}
model MediaData {
id Int @id
pixel_width Int?
@@ -212,7 +230,6 @@ model Tag {
id Int @id @default(autoincrement())
pub_id String @unique
name String?
encryption Int? @default(0)
total_files Int? @default(0)
redundancy_goal Int? @default(1)
date_created DateTime @default(now())
@@ -260,10 +277,10 @@ model LabelOnFile {
}
model Job {
id String @id
client_id Int
action Int
status Int @default(0)
id String @id
node_id Int
action Int
status Int @default(0)
task_count Int @default(1)
completed_task_count Int @default(0)
@@ -271,23 +288,10 @@ model Job {
date_modified DateTime @default(now())
seconds_elapsed Int @default(0)
clients Client @relation(fields: [client_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
nodes Node @relation(fields: [node_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@map("jobs")
}
model Space {
id Int @id @default(autoincrement())
pub_id String @unique
name String
encryption Int? @default(0) // remove
date_created DateTime @default(now())
date_modified DateTime @default(now())
Library Library? @relation(fields: [libraryId], references: [id])
libraryId Int?
@@map("spaces")
}
model Album {
id Int @id @default(autoincrement())
pub_id String @unique
-113
View File
@@ -1,113 +0,0 @@
use crate::{
prisma::{self, client},
state, Core, CoreContext,
};
use chrono::{DateTime, Utc};
use int_enum::IntEnum;
use serde::{Deserialize, Serialize};
use std::env;
use thiserror::Error;
use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct Client {
pub uuid: String,
pub name: String,
pub platform: Platform,
pub tcp_address: String,
#[ts(type = "string")]
pub last_seen: DateTime<Utc>,
#[ts(type = "string")]
pub last_synchronized: DateTime<Utc>,
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, TS, Eq, PartialEq, IntEnum)]
#[ts(export)]
pub enum Platform {
Unknown = 0,
Windows = 1,
MacOS = 2,
Linux = 3,
IOS = 4,
Android = 5,
}
// impl Into<Client> for client::Data {
// fn into(self) -> Client {
// Client {
// uuid: self.pub_id,
// name: self.name,
// platform: ,
// tcp_address: self.tcp_address,
// last_seen: self.last_seen,
// last_synchronized: self.last_synchronized,
// }
// }
// }
pub async fn create(core: &Core) -> Result<(), ClientError> {
println!("Creating client...");
let mut config = state::client::get();
let db = &core.database;
let hostname = match hostname::get() {
Ok(hostname) => hostname.to_str().unwrap_or_default().to_owned(),
Err(_) => "unknown".to_owned(),
};
let platform = match env::consts::OS {
"windows" => Platform::Windows,
"macos" => Platform::MacOS,
"linux" => Platform::Linux,
_ => Platform::Unknown,
};
let client = match db
.client()
.find_unique(client::pub_id::equals(config.client_uuid.clone()))
.exec()
.await?
{
Some(client) => client,
None => {
db.client()
.create(
client::pub_id::set(config.client_uuid.clone()),
client::name::set(hostname.clone()),
vec![
client::platform::set(platform as i32),
client::online::set(Some(true)),
],
)
.exec()
.await?
}
};
config.client_name = hostname;
config.client_id = client.id;
config.save();
println!("Client: {:?}", &client);
Ok(())
}
pub async fn get_clients(ctx: &CoreContext) -> Result<Vec<client::Data>, ClientError> {
let db = &ctx.database;
let client = db.client().find_many(vec![]).exec().await?;
Ok(client)
}
#[derive(Error, Debug)]
pub enum ClientError {
#[error("Database error")]
DatabaseError(#[from] prisma::QueryError),
#[error("Client not found error")]
ClientNotFound,
}
-1
View File
@@ -1,4 +1,3 @@
use crate::state;
use crate::{prisma, prisma::PrismaClient};
use thiserror::Error;
pub mod migrate;
+2 -2
View File
@@ -1,5 +1,5 @@
use crate::job::jobs::JobReportUpdate;
use crate::state::client;
use crate::node::state;
use crate::{
job::{jobs::Job, worker::WorkerContext},
prisma::file_path,
@@ -27,7 +27,7 @@ pub static THUMBNAIL_CACHE_DIR_NAME: &str = "thumbnails";
#[async_trait::async_trait]
impl Job for ThumbnailJob {
async fn run(&self, ctx: WorkerContext) -> Result<()> {
let config = client::get();
let config = state::get();
let core_ctx = ctx.core_ctx.clone();
let location = sys::locations::get_location(&core_ctx, self.location_id).await?;
+21 -16
View File
@@ -1,13 +1,12 @@
use std::path::Path;
use crate::{
encode::thumb::THUMBNAIL_CACHE_DIR_NAME,
file::{DirectoryWithContents, FileError, FilePath},
file::{DirectoryWithContents, File, FileError},
node::state,
prisma::file_path,
state::client,
sys::locations::get_location,
CoreContext,
};
use std::path::Path;
pub async fn open_dir(
ctx: &CoreContext,
@@ -15,7 +14,7 @@ pub async fn open_dir(
path: &str,
) -> Result<DirectoryWithContents, FileError> {
let db = &ctx.database;
let config = client::get();
let config = state::get();
// get location
let location = get_location(ctx, location_id.clone()).await?;
@@ -34,24 +33,30 @@ pub async fn open_dir(
let files = db
.file_path()
.find_many(vec![file_path::parent_id::equals(Some(directory.id))])
.with(file_path::file::fetch())
.exec()
.await?;
let files: Vec<FilePath> = files.into_iter().map(|l| l.into()).collect();
// convert database structs into a File
let files: Vec<File> = files
.into_iter()
.map(|l| {
let mut file: File = l.file().unwrap_or_default().unwrap().clone().into();
file.paths.push(l.into());
file
})
.collect();
let mut contents: Vec<FilePath> = vec![];
let mut contents: Vec<File> = vec![];
for mut file in files {
if file.temp_cas_id.is_some() {
let path = Path::new(&config.data_path)
.join(THUMBNAIL_CACHE_DIR_NAME)
.join(format!("{}", location.id))
.join(file.temp_cas_id.as_ref().unwrap())
.with_extension("webp");
let thumb_path = Path::new(&config.data_path)
.join(THUMBNAIL_CACHE_DIR_NAME)
.join(format!("{}", location.id))
.join(file.cas_id.clone())
.with_extension("webp");
let exists = path.exists();
file.has_local_thumbnail = exists;
}
file.has_thumbnail = thumb_path.exists();
contents.push(file);
}
+35 -28
View File
@@ -15,6 +15,8 @@ pub enum ScanProgress {
Message(String),
}
static BATCH_SIZE: usize = 100;
// creates a vector of valid path buffers from a directory
pub async fn scan_path(
ctx: &CoreContext,
@@ -49,7 +51,7 @@ pub async fn scan_path(
// spawn a dedicated thread to scan the directory for performance
let (paths, scan_start, on_progress) = tokio::task::spawn_blocking(move || {
// store every valid path discovered
let mut paths: Vec<(PathBuf, i32, Option<i32>)> = Vec::new();
let mut paths: Vec<(PathBuf, i32, Option<i32>, bool)> = Vec::new();
// store a hashmap of directories to their file ids for fast lookup
let mut dirs: HashMap<String, i32> = HashMap::new();
// begin timer for logging purposes
@@ -76,6 +78,8 @@ pub async fn scan_path(
};
let path = entry.path();
println!("found: {:?}", path);
let parent_path = path
.parent()
.unwrap_or(Path::new(""))
@@ -93,16 +97,18 @@ pub async fn scan_path(
on_progress(vec![
ScanProgress::Message(format!("{}", str)),
ScanProgress::ChunkCount(paths.len() / 100),
ScanProgress::ChunkCount(paths.len() / BATCH_SIZE),
]);
let file_id = get_id();
let file_type = entry.file_type();
let is_dir = file_type.is_dir();
if entry.file_type().is_dir() || entry.file_type().is_file() {
paths.push((path.to_owned(), file_id, parent_dir_id.cloned()));
if is_dir || file_type.is_file() {
paths.push((path.to_owned(), file_id, parent_dir_id.cloned(), is_dir));
}
if entry.file_type().is_dir() {
if is_dir {
let _path = match path.to_str() {
Some(path) => path.to_owned(),
None => continue,
@@ -118,11 +124,11 @@ pub async fn scan_path(
let db_write_start = Instant::now();
let scan_read_time = scan_start.elapsed();
for (i, chunk) in paths.chunks(100).enumerate() {
for (i, chunk) in paths.chunks(BATCH_SIZE).enumerate() {
on_progress(vec![
ScanProgress::SavedChunks(i as usize),
ScanProgress::Message(format!(
"Writing {} of {} to db",
"Writing {} of {} to library",
i * chunk.len(),
paths.len(),
)),
@@ -130,9 +136,9 @@ pub async fn scan_path(
// vector to store active models
let mut files: Vec<String> = Vec::new();
for (file_path, file_id, parent_dir_id) in chunk {
for (file_path, file_id, parent_dir_id, is_dir) in chunk {
files.push(
match prepare_values(&file_path, *file_id, &location, parent_dir_id) {
match prepare_values(&file_path, *file_id, &location, parent_dir_id, *is_dir) {
Ok(file) => file,
Err(e) => {
println!("Error creating file model from path {:?}: {}", file_path, e);
@@ -143,7 +149,7 @@ pub async fn scan_path(
}
let raw_sql = format!(
r#"
INSERT INTO file_paths (id, is_dir, location_id, materialized_path, name, extension, parent_id, date_created, temp_cas_id)
INSERT INTO file_paths (id, is_dir, location_id, materialized_path, name, extension, parent_id)
VALUES {}
"#,
files.join(", ")
@@ -168,8 +174,9 @@ fn prepare_values(
id: i32,
location: &LocationResource,
parent_id: &Option<i32>,
is_dir: bool,
) -> Result<String> {
let metadata = fs::metadata(&file_path)?;
// let metadata = fs::metadata(&file_path)?;
let location_path = location.path.as_ref().unwrap().as_str();
// let size = metadata.len();
let name;
@@ -179,7 +186,7 @@ fn prepare_values(
// if 'file_path' is a directory, set extension to an empty string to avoid periods in folder names
// - being interpreted as file extensions
if file_path.is_dir() {
if is_dir {
extension = "".to_string();
name = extract_name(file_path.file_name());
} else {
@@ -196,24 +203,24 @@ fn prepare_values(
None => return Err(anyhow!("{}", file_path.to_str().unwrap_or_default())),
};
let cas_id = {
if !metadata.is_dir() {
// TODO: remove unwrap, skip and make sure to continue loop
let mut x = generate_cas_id(&file_path.to_str().unwrap(), metadata.len()).unwrap();
x.truncate(16);
x
} else {
"".to_string()
}
};
// let cas_id = {
// if !metadata.is_dir() {
// // TODO: remove unwrap, skip and make sure to continue loop
// let mut x = generate_cas_id(&file_path.to_str().unwrap(), metadata.len()).unwrap();
// x.truncate(16);
// x
// } else {
// "".to_string()
// }
// };
let date_created: DateTime<Utc> = metadata.created().unwrap().into();
let parsed_date_created = date_created.to_rfc3339_opts(SecondsFormat::Millis, true);
// let date_created: DateTime<Utc> = metadata.created().unwrap().into();
// let parsed_date_created = date_created.to_rfc3339_opts(SecondsFormat::Millis, true);
let values = format!(
"({}, {}, {}, \"{}\", \"{}\", \"{}\", {},\"{}\", \"{}\")",
"({}, {}, {}, \"{}\", \"{}\", \"{}\", {})",
id,
metadata.is_dir(),
is_dir,
location.id,
materialized_path,
name,
@@ -222,8 +229,8 @@ fn prepare_values(
.clone()
.map(|id| format!("\"{}\"", &id))
.unwrap_or("NULL".to_string()),
parsed_date_created,
cas_id
// parsed_date_created,
// cas_id
);
println!("{}", values);
+6 -7
View File
@@ -29,7 +29,7 @@ pub struct File {
pub has_thumbnail: bool,
pub has_thumbstrip: bool,
pub has_video_preview: bool,
pub encryption: EncryptionAlgorithm,
// pub encryption: EncryptionAlgorithm,
pub ipfs_id: Option<String>,
pub comment: Option<String>,
@@ -58,7 +58,7 @@ pub struct FilePath {
pub extension: Option<String>,
pub file_id: Option<i32>,
pub parent_id: Option<i32>,
pub temp_cas_id: Option<String>,
// pub temp_cas_id: Option<String>,
pub has_local_thumbnail: bool,
#[ts(type = "string")]
pub date_created: chrono::DateTime<chrono::Utc>,
@@ -66,7 +66,6 @@ pub struct FilePath {
pub date_modified: chrono::DateTime<chrono::Utc>,
#[ts(type = "string")]
pub date_indexed: chrono::DateTime<chrono::Utc>,
pub permissions: Option<String>,
}
#[repr(i32)]
@@ -92,7 +91,7 @@ impl Into<File> for file::Data {
integrity_checksum: self.integrity_checksum,
kind: IntEnum::from_int(self.kind).unwrap(),
size_in_bytes: self.size_in_bytes.to_string(),
encryption: EncryptionAlgorithm::from_int(self.encryption).unwrap(),
// encryption: EncryptionAlgorithm::from_int(self.encryption).unwrap(),
ipfs_id: self.ipfs_id,
hidden: self.hidden,
favorite: self.favorite,
@@ -119,11 +118,11 @@ impl Into<FilePath> for file_path::Data {
parent_id: self.parent_id,
location_id: self.location_id,
date_indexed: self.date_indexed,
permissions: self.permissions,
// permissions: self.permissions,
has_local_thumbnail: false,
name: self.name,
extension: self.extension,
temp_cas_id: self.temp_cas_id,
// temp_cas_id: self.temp_cas_id,
date_created: self.date_created,
date_modified: self.date_modified,
}
@@ -134,7 +133,7 @@ impl Into<FilePath> for file_path::Data {
#[ts(export)]
pub struct DirectoryWithContents {
pub directory: FilePath,
pub contents: Vec<FilePath>,
pub contents: Vec<File>,
}
#[derive(Error, Debug)]
+4 -4
View File
@@ -3,8 +3,8 @@ use super::{
JobError,
};
use crate::{
prisma::{client, job},
state,
node::state,
prisma::{job, node},
sync::{crdt::Replicate, engine::SyncContext},
CoreContext,
};
@@ -134,14 +134,14 @@ impl JobReport {
}
}
pub async fn create(&self, ctx: &CoreContext) -> Result<(), JobError> {
let config = state::client::get();
let config = state::get();
ctx
.database
.job()
.create(
job::id::set(self.id.clone()),
job::action::set(1),
job::clients::link(client::id::equals(config.client_id)),
job::nodes::link(node::id::equals(config.node_id)),
vec![],
)
.exec()
+24 -22
View File
@@ -1,9 +1,11 @@
use crate::{file::cas::identifier::FileIdentifierJob, library::loader::get_library_path};
use crate::{
file::cas::identifier::FileIdentifierJob, library::loader::get_library_path,
node::state::NodeState,
};
use job::jobs::{Job, JobReport, Jobs};
use prisma::PrismaClient;
use serde::{Deserialize, Serialize};
use state::client::ClientState;
use std::{fs, path::Path, sync::Arc};
use std::{fs, sync::Arc};
use thiserror::Error;
use tokio::sync::{
mpsc::{self, unbounded_channel, UnboundedReceiver, UnboundedSender},
@@ -14,17 +16,16 @@ use ts_rs::TS;
use crate::encode::thumb::ThumbnailJob;
// init modules
pub mod client;
pub mod crypto;
pub mod db;
pub mod encode;
pub mod file;
pub mod job;
pub mod library;
pub mod node;
#[cfg(target_os = "p2p")]
pub mod p2p;
pub mod prisma;
pub mod state;
pub mod sync;
pub mod sys;
pub mod util;
@@ -101,8 +102,8 @@ impl CoreContext {
}
}
pub struct Core {
state: ClientState,
pub struct Node {
state: NodeState,
jobs: job::jobs::Jobs,
database: Arc<PrismaClient>,
// filetype_registry: library::TypeRegistry,
@@ -126,9 +127,9 @@ pub struct Core {
),
}
impl Core {
// create new instance of core, run startup tasks
pub async fn new(mut data_dir: std::path::PathBuf) -> (Core, mpsc::Receiver<CoreEvent>) {
impl Node {
// create new instance of node, run startup tasks
pub async fn new(mut data_dir: std::path::PathBuf) -> (Node, mpsc::Receiver<CoreEvent>) {
let (event_sender, event_recv) = mpsc::channel(100);
data_dir = data_dir.join("spacedrive");
@@ -136,15 +137,15 @@ impl Core {
// create data directory if it doesn't exist
fs::create_dir_all(&data_dir).unwrap();
// prepare basic client state
let mut state = ClientState::new(data_dir, "diamond-mastering-space-dragon").unwrap();
let mut state = NodeState::new(data_dir, "diamond-mastering-space-dragon").unwrap();
// load from disk
state.read_disk().unwrap_or(println!(
"Error: No client state found, creating new one..."
));
state
.read_disk()
.unwrap_or(println!("Error: No node state found, creating new one..."));
state.save();
println!("Client State: {:?}", state);
println!("Node State: {:?}", state);
// connect to default library
let database = Arc::new(
@@ -155,7 +156,7 @@ impl Core {
let internal_channel = unbounded_channel::<InternalEvent>();
let core = Core {
let node = Node {
state,
query_channel: unbounded_channel(),
command_channel: unbounded_channel(),
@@ -170,7 +171,7 @@ impl Core {
p2p::listener::listen(None).await.unwrap_or(());
});
(core, event_recv)
(node, event_recv)
}
pub fn get_context(&self) -> CoreContext {
@@ -233,10 +234,10 @@ impl Core {
}
}
}
// init client
match client::create(&self).await {
// init node data within library
match node::LibraryNode::create(&self).await {
Ok(_) => println!("Spacedrive online"),
Err(e) => println!("Error initializing client: {:?}", e),
Err(e) => println!("Error initializing node: {:?}", e),
};
}
@@ -314,11 +315,11 @@ impl Core {
),
ClientQuery::LibGetTags => todo!(),
ClientQuery::JobGetRunning => CoreResponse::JobGetRunning(self.jobs.get_running().await),
// TODO: FIX THIS
ClientQuery::JobGetHistory => CoreResponse::JobGetHistory(Jobs::get_history(&ctx).await?),
ClientQuery::GetLibraryStatistics => {
CoreResponse::GetLibraryStatistics(library::statistics::Statistics::calculate(&ctx).await?)
}
ClientQuery::GetNodes => todo!(),
})
}
}
@@ -370,6 +371,7 @@ pub enum ClientQuery {
limit: i32,
},
GetLibraryStatistics,
GetNodes,
}
// represents an event this library can emit
@@ -395,7 +397,7 @@ pub enum CoreResponse {
SysGetLocation(sys::locations::LocationResource),
SysGetLocations(Vec<sys::locations::LocationResource>),
LibGetExplorerDir(file::DirectoryWithContents),
ClientGetState(ClientState),
ClientGetState(NodeState),
LocCreate(sys::locations::LocationResource),
JobGetRunning(Vec<JobReport>),
JobGetHistory(Vec<JobReport>),
+7 -7
View File
@@ -1,9 +1,9 @@
use anyhow::Result;
use uuid::Uuid;
use crate::state::client::LibraryState;
use crate::{db::migrate, prisma::library, state};
use crate::{Core, CoreContext};
use crate::node::state::LibraryState;
use crate::{db::migrate, node::state, prisma::library};
use crate::{CoreContext, Node};
use super::LibraryError;
@@ -15,8 +15,8 @@ pub fn get_library_path(data_path: &str) -> String {
format!("{}/{}", path, LIBRARY_DB_NAME)
}
pub async fn get(core: &Core) -> Result<library::Data, LibraryError> {
let config = state::client::get();
pub async fn get(core: &Node) -> Result<library::Data, LibraryError> {
let config = state::get();
let db = &core.database;
let library_state = config.get_current_library();
@@ -43,7 +43,7 @@ pub async fn get(core: &Core) -> Result<library::Data, LibraryError> {
}
pub async fn load(ctx: &CoreContext, library_path: &str, library_id: &str) -> Result<()> {
let mut config = state::client::get();
let mut config = state::get();
println!("Initializing library: {} {}", &library_id, library_path);
@@ -58,7 +58,7 @@ pub async fn load(ctx: &CoreContext, library_path: &str, library_id: &str) -> Re
}
pub async fn create(ctx: &CoreContext, name: Option<String>) -> Result<()> {
let mut config = state::client::get();
let mut config = state::get();
let uuid = Uuid::new_v4().to_string();
+3 -3
View File
@@ -1,6 +1,6 @@
use crate::{
node::state,
prisma::{library, library_statistics::*},
state::client,
sys::{self, volumes::Volume},
CoreContext,
};
@@ -53,7 +53,7 @@ impl Default for Statistics {
impl Statistics {
pub async fn retrieve(ctx: &CoreContext) -> Result<Statistics, LibraryError> {
let config = client::get();
let config = state::get();
let db = &ctx.database;
let library_data = config.get_current_library();
@@ -70,7 +70,7 @@ impl Statistics {
Ok(library_statistics_db.into())
}
pub async fn calculate(ctx: &CoreContext) -> Result<Statistics, LibraryError> {
let config = client::get();
let config = state::get();
let db = &ctx.database;
// get library from client state
let library_data = config.get_current_library();
+104
View File
@@ -0,0 +1,104 @@
use crate::{
prisma::{self, node},
CoreContext, Node,
};
use chrono::{DateTime, Utc};
use int_enum::IntEnum;
use serde::{Deserialize, Serialize};
use std::env;
use thiserror::Error;
use ts_rs::TS;
pub mod state;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct LibraryNode {
pub uuid: String,
pub name: String,
pub platform: Platform,
pub tcp_address: String,
#[ts(type = "string")]
pub last_seen: DateTime<Utc>,
#[ts(type = "string")]
pub last_synchronized: DateTime<Utc>,
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, TS, Eq, PartialEq, IntEnum)]
#[ts(export)]
pub enum Platform {
Unknown = 0,
Windows = 1,
MacOS = 2,
Linux = 3,
IOS = 4,
Android = 5,
}
impl LibraryNode {
pub async fn create(node: &Node) -> Result<(), NodeError> {
println!("Creating node...");
let mut config = state::get();
let db = &node.database;
let hostname = match hostname::get() {
Ok(hostname) => hostname.to_str().unwrap_or_default().to_owned(),
Err(_) => "unknown".to_owned(),
};
let platform = match env::consts::OS {
"windows" => Platform::Windows,
"macos" => Platform::MacOS,
"linux" => Platform::Linux,
_ => Platform::Unknown,
};
let _node = match db
.node()
.find_unique(node::pub_id::equals(config.node_pub_id.clone()))
.exec()
.await?
{
Some(node) => node,
None => {
db.node()
.create(
node::pub_id::set(config.node_pub_id.clone()),
node::name::set(hostname.clone()),
vec![
node::platform::set(platform as i32),
node::online::set(Some(true)),
],
)
.exec()
.await?
}
};
config.node_name = hostname;
config.node_id = _node.id;
config.save();
println!("node: {:?}", &_node);
Ok(())
}
pub async fn get_nodes(ctx: &CoreContext) -> Result<Vec<node::Data>, NodeError> {
let db = &ctx.database;
let _node = db.node().find_many(vec![]).exec().await?;
Ok(_node)
}
}
#[derive(Error, Debug)]
pub enum NodeError {
#[error("Database error")]
DatabaseError(#[from] prisma::QueryError),
#[error("Client not found error")]
ClientNotFound,
}
@@ -9,23 +9,21 @@ use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone, Default, TS)]
#[ts(export)]
pub struct ClientState {
// client id is a uniquely generated UUID
pub client_uuid: String,
pub client_id: i32,
// client_name is the name of the device running the client
pub client_name: String,
pub struct NodeState {
pub node_pub_id: String,
pub node_id: i32,
pub node_name: String,
// config path is stored as struct can exist only in memory during startup and be written to disk later without supplying path
pub data_path: String,
// the port this client uses to listen for incoming connections
// the port this node uses to listen for incoming connections
pub tcp_port: u32,
// all the libraries loaded by this client
// all the libraries loaded by this node
pub libraries: Vec<LibraryState>,
// used to quickly find the default library
pub current_library_uuid: String,
}
pub static CLIENT_STATE_CONFIG_NAME: &str = "client_state.json";
pub static NODE_STATE_CONFIG_NAME: &str = "node_state.json";
#[derive(Debug, Serialize, Deserialize, Clone, Default, TS)]
#[ts(export)]
@@ -36,26 +34,26 @@ pub struct LibraryState {
pub offline: bool,
}
// global, thread-safe storage for client state
// global, thread-safe storage for node state
lazy_static! {
static ref CONFIG: RwLock<Option<ClientState>> = RwLock::new(None);
static ref CONFIG: RwLock<Option<NodeState>> = RwLock::new(None);
}
pub fn get() -> ClientState {
pub fn get() -> NodeState {
match CONFIG.read() {
Ok(guard) => guard.clone().unwrap_or(ClientState::default()),
Err(_) => return ClientState::default(),
Ok(guard) => guard.clone().unwrap_or(NodeState::default()),
Err(_) => return NodeState::default(),
}
}
impl ClientState {
pub fn new(data_path: &str, client_name: &str) -> Result<Self> {
impl NodeState {
pub fn new(data_path: &str, node_name: &str) -> Result<Self> {
let uuid = Uuid::new_v4().to_string();
// create struct and assign defaults
let config = Self {
client_uuid: uuid,
node_pub_id: uuid,
data_path: data_path.to_string(),
client_name: client_name.to_string(),
node_name: node_name.to_string(),
..Default::default()
};
Ok(config)
@@ -65,7 +63,7 @@ impl ClientState {
self.write_memory();
// only write to disk if config path is set
if !&self.data_path.is_empty() {
let config_path = format!("{}/{}", &self.data_path, CLIENT_STATE_CONFIG_NAME);
let config_path = format!("{}/{}", &self.data_path, NODE_STATE_CONFIG_NAME);
let mut file = fs::File::create(config_path).unwrap();
let json = serde_json::to_string(&self).unwrap();
file.write_all(json.as_bytes()).unwrap();
@@ -73,7 +71,7 @@ impl ClientState {
}
pub fn read_disk(&mut self) -> Result<()> {
let config_path = format!("{}/{}", &self.data_path, CLIENT_STATE_CONFIG_NAME);
let config_path = format!("{}/{}", &self.data_path, NODE_STATE_CONFIG_NAME);
// open the file and parse json
let file = fs::File::open(config_path)?;
let reader = BufReader::new(file);
-1
View File
@@ -1 +0,0 @@
pub mod client;
+2 -2
View File
@@ -1,5 +1,5 @@
use crate::{
file::indexer::IndexerJob, prisma::location, state::client, ClientQuery, CoreContext, CoreEvent,
file::indexer::IndexerJob, node::state, prisma::location, ClientQuery, CoreContext, CoreEvent,
};
use anyhow::Result;
use serde::{Deserialize, Serialize};
@@ -109,7 +109,7 @@ pub async fn get_locations(ctx: &CoreContext) -> Result<Vec<LocationResource>, S
pub async fn create_location(ctx: &CoreContext, path: &str) -> Result<LocationResource, SysError> {
let db = &ctx.database;
let config = client::get();
let config = state::get();
// check if we have access to this location
if !Path::new(path).exists() {
+5 -5
View File
@@ -1,5 +1,5 @@
// use crate::native;
use crate::{prisma::volume::*, state::client};
use crate::{node::state, prisma::volume::*};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
// #[cfg(not(target_os = "macos"))]
@@ -28,20 +28,20 @@ pub struct Volume {
impl Volume {
pub async fn save(ctx: &CoreContext) -> Result<(), SysError> {
let db = &ctx.database;
let config = client::get();
let config = state::get();
let volumes = Self::get_volumes()?;
// enter all volumes associate with this client add to db
for volume in volumes {
db.volume()
.upsert(client_id_mount_point_name(
config.client_id.clone(),
.upsert(node_id_mount_point_name(
config.node_id.clone(),
volume.mount_point.to_string(),
volume.name.to_string(),
))
.create(
client_id::set(config.client_id),
node_id::set(config.node_id),
name::set(volume.name),
mount_point::set(volume.mount_point),
vec![
+1
View File
@@ -34,6 +34,7 @@
"phosphor-react": "^1.4.1",
"pretty-bytes": "^6.0.0",
"react": "^18.0.0",
"react-countup": "^6.2.0",
"react-dom": "^18.0.0",
"react-dropzone": "^12.0.4",
"react-error-boundary": "^3.1.4",
-1
View File
@@ -36,7 +36,6 @@ import ExperimentalSettings from './screens/settings/ExperimentalSettings';
import { TagScreen } from './screens/Tag';
const queryClient = new QueryClient();
export const AppPropsContext = React.createContext<AppProps | null>(null);
@@ -39,7 +39,7 @@ export function Device(props: DeviceProps) {
{props.type === 'laptop' && <Laptop weight="fill" size={20} className="mr-2" />}
{props.type === 'desktop' && <Desktop weight="fill" size={20} className="mr-2" />}
{props.type === 'server' && <Cloud weight="fill" size={20} className="mr-2" />}
<h3 className="font-semibold text-md">{props.name}</h3>
<h3 className="font-semibold text-md">{props.name || 'Unnamed Device'}</h3>
<div className="flex flex-row space-x-1.5 mt-0.5">
<span className="font-semibold flex flex-row h-[19px] -mt-0.5 ml-3 py-0.5 px-1.5 text-[10px] rounded bg-gray-500 text-gray-400">
<LockClosedIcon className="w-3 h-3 mr-1 -ml-0.5 m-[1px]" />
Whitespace-only changes.
@@ -1,7 +1,7 @@
import { LockClosedIcon } from '@heroicons/react/outline';
import { CameraIcon, LockClosedIcon, PhotographIcon } from '@heroicons/react/outline';
import { CogIcon, EyeOffIcon, PlusIcon, ServerIcon } from '@heroicons/react/solid';
import clsx from 'clsx';
import { CirclesFour, Code, EjectSimple, MonitorPlay, Planet } from 'phosphor-react';
import { Camera, CirclesFour, Code, EjectSimple, MonitorPlay, Planet } from 'phosphor-react';
import React, { useContext, useEffect, useState } from 'react';
import { NavLink, NavLinkProps } from 'react-router-dom';
import { TrafficLights } from '../os/TrafficLights';
@@ -77,7 +77,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
return (
<div className="flex flex-col flex-grow-0 flex-shrink-0 w-48 min-h-full px-3 overflow-x-hidden overflow-y-scroll border-r border-gray-100 no-scrollbar bg-gray-50 dark:bg-gray-850 dark:border-gray-600">
{appPropsContext?.platform === 'macOS' ? (
{appPropsContext?.platform === 'macOS' || appPropsContext?.demoMode ? (
<>
<MacOSTrafficLights />
</>
@@ -120,6 +120,10 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<Icon component={CirclesFour} />
Content
</SidebarLink>
<SidebarLink to="photos">
<Icon component={PhotographIcon} />
Photos
</SidebarLink>
{experimental ? (
<SidebarLink to="debug">
@@ -44,6 +44,7 @@ const Light: React.FC<LightProps> = (props) => {
})}
>
{(() => {
if (!props.focused) return <></>;
switch (props.mode) {
case 'close':
return (
+64 -14
View File
@@ -1,15 +1,18 @@
import { CloudIcon } from '@heroicons/react/outline';
import { CogIcon, MenuIcon, PlusIcon } from '@heroicons/react/solid';
import { useBridgeQuery } from '@sd/client';
import { Statistics } from '@sd/core';
import { Button } from '@sd/ui';
import byteSize from 'byte-size';
import { DotsSixVertical, Laptop, LineSegments, Plus } from 'phosphor-react';
import React, { useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { AppPropsContext } from '../App';
import { Device } from '../components/device/Device';
import FileItem from '../components/file/FileItem';
import Dialog from '../components/layout/Dialog';
import { Input } from '../components/primitive';
import { InputContainer } from '../components/primitive/InputContainer';
import { useCountUp } from 'react-countup';
interface StatItemProps {
name: string;
@@ -18,12 +21,34 @@ interface StatItemProps {
}
const StatItem: React.FC<StatItemProps> = (props) => {
const countUpRef = React.useRef(null);
let size = byteSize(Number(props.value) || 0);
let amount = parseFloat(size.value);
const [hasRun, setHasRun] = useState(false);
useCountUp({
startOnMount: !hasRun,
ref: countUpRef,
start: amount / 2,
end: amount,
delay: 0.1,
decimals: 1,
duration: 2,
enableScrollSpy: true,
useEasing: true,
onEnd: () => {
setHasRun(true);
}
});
return (
<div className="flex flex-col px-4 py-3 duration-75 transform rounded-md cursor-default hover:bg-gray-50 hover:dark:bg-gray-600">
<div className="flex flex-col flex-shrink-0 w-32 px-4 py-3 duration-75 transform rounded-md cursor-default hover:bg-gray-50 hover:dark:bg-gray-600">
<span className="text-sm text-gray-400">{props.name}</span>
<span className="text-2xl font-bold">
{size.value}
<span ref={countUpRef} />
<span className="ml-1 text-[16px] text-gray-400">{size.unit}</span>
</span>
</div>
@@ -34,39 +59,63 @@ export const OverviewScreen: React.FC<{}> = (props) => {
const { data: libraryStatistics } = useBridgeQuery('GetLibraryStatistics');
const { data: clientState } = useBridgeQuery('ClientGetState');
const [stats, setStats] = useState<Statistics>(libraryStatistics || ({} as Statistics));
// get app props context
const appPropsContext = useContext(AppPropsContext);
useEffect(() => {
if (appPropsContext?.demoMode == true && !libraryStatistics?.library_db_size) {
setStats({
total_bytes_capacity: '8093333345230',
preview_media_bytes: '2304387532',
library_db_size: '83345230',
total_file_count: 20342345,
total_bytes_free: '89734502034',
total_bytes_used: '8093333345230',
total_unique_bytes: '9347397'
});
}
}, [appPropsContext, libraryStatistics]);
return (
<div className="flex flex-col w-full h-screen overflow-x-hidden custom-scroll page-scroll">
<div data-tauri-drag-region className="flex flex-shrink-0 w-full h-7" />
<div data-tauri-drag-region className="flex flex-shrink-0 w-full h-5" />
{/* PAGE */}
<div className="flex flex-col w-full h-screen px-3">
{/* STAT HEADER */}
<div className="flex w-full">
<div className="flex flex-wrap flex-grow pb-4 space-x-6">
{/* STAT CONTAINER */}
<div className="flex pb-4 overflow-hidden">
<StatItem
name="Total capacity"
value={libraryStatistics?.total_bytes_capacity}
unit={libraryStatistics?.total_bytes_capacity}
value={stats?.total_bytes_capacity}
unit={stats?.total_bytes_capacity}
/>
<StatItem
name="Index size"
value={libraryStatistics?.library_db_size}
unit={libraryStatistics?.library_db_size}
value={stats?.library_db_size}
unit={stats?.library_db_size}
/>
<StatItem
name="Preview media"
value={libraryStatistics?.preview_media_bytes}
unit={libraryStatistics?.preview_media_bytes}
value={stats?.preview_media_bytes}
unit={stats?.preview_media_bytes}
/>
<StatItem
name="Free space"
value={libraryStatistics?.total_bytes_free}
unit={libraryStatistics?.total_bytes_free}
value={stats?.total_bytes_free}
unit={stats?.total_bytes_free}
/>
<StatItem name="Total at-risk" value={'0'} unit={stats?.preview_media_bytes} />
{/* <StatItem
name="Total at-risk"
value={'0'}
unit={libraryStatistics?.preview_media_bytes}
unit={stats?.preview_media_bytes}
/>
<StatItem name="Total backed up" value={'0'} unit={''} /> */}
</div>
<div className="flex-grow" />
<div className="space-x-2">
<Dialog
title="Add Device"
@@ -78,6 +127,7 @@ export const OverviewScreen: React.FC<{}> = (props) => {
size="sm"
icon={<PlusIcon className="inline w-4 h-4 -mt-0.5 mr-1" />}
variant="gray"
className="hidden sm:visible"
>
Add Device
</Button>
@@ -21,7 +21,7 @@ export default function ExperimentalSettings() {
title="Debug Menu"
description="Shows data about Spacedrive such as Jobs, Job History and Client State."
>
<div className="flex items-center h-full">
<div className="flex items-center h-full pl-10">
<Toggle initialState={experimental} size={'sm'} type="experimental" />
</div>
</InputContainer>
@@ -22,8 +22,8 @@ export default function LibrarySettings() {
title="Encrypt on cloud"
description="Enable if library contains sensitive data and should not be synced to the cloud without full encryption."
>
<div className="flex items-center h-full">
<Toggle initialState={true} size={'sm'} />
<div className="flex items-center h-full pl-10">
<Toggle initialState={true} size={'sm'} type={''} />
</div>
</InputContainer>
</div>
-40
View File
@@ -17,43 +17,3 @@
left: 50%;
transform: translate(-50%, -50%);
}
.landing-img {
background-image: url('/app.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.fade-in-image {
animation: fadeIn 1s;
-webkit-animation: fadeIn 1s;
-moz-animation: fadeIn 1s;
-o-animation: fadeIn 1s;
-ms-animation: fadeIn 1s;
}
@keyframes fadeIn {
0% {opacity:0;}
100% {opacity:1;}
}
@-moz-keyframes fadeIn {
0% {opacity:0;}
100% {opacity:1;}
}
@-webkit-keyframes fadeIn {
0% {opacity:0;}
100% {opacity:1;}
}
@-o-keyframes fadeIn {
0% {opacity:0;}
100% {opacity:1;}
}
@-ms-keyframes fadeIn {
0% {opacity:0;}
100% {opacity:1;}
}
+101 -86
View File
@@ -1,4 +1,4 @@
lockfileVersion: 5.4
lockfileVersion: 5.3
importers:
@@ -115,7 +115,7 @@ importers:
vite-plugin-svgr: ^1.1.0
dependencies:
'@fontsource/inter': 4.5.7
'@headlessui/react': 1.5.0_zpnidt7m3osuk7shl3s4oenomq
'@headlessui/react': 1.5.0_react-dom@18.0.0+react@18.0.0
'@heroicons/react': 1.0.6_react@18.0.0
'@icons-pack/react-simple-icons': 4.6.1_react@18.0.0
'@sd/client': link:../../packages/client
@@ -131,10 +131,10 @@ importers:
phosphor-react: 1.4.1_react@18.0.0
prismjs: 1.28.0
react: 18.0.0
react-device-detect: 2.2.2_zpnidt7m3osuk7shl3s4oenomq
react-device-detect: 2.2.2_react-dom@18.0.0+react@18.0.0
react-dom: 18.0.0_react@18.0.0
react-helmet: 6.1.0_react@18.0.0
react-router-dom: 6.3.0_zpnidt7m3osuk7shl3s4oenomq
react-router-dom: 6.3.0_react-dom@18.0.0+react@18.0.0
react-tsparticles: 2.0.6_react@18.0.0
simple-icons: 6.19.0
tsparticles: 2.0.6
@@ -204,7 +204,7 @@ importers:
typescript: 4.6.3
vite: 2.9.5
vite-plugin-svgr: 1.1.0_vite@2.9.5
vite-plugin-tsconfig-paths: 1.0.5_w5v33ehmxzy52jcrbupop25zpy
vite-plugin-tsconfig-paths: 1.0.5_typescript@4.6.3+vite@2.9.5
core:
specifiers:
@@ -213,7 +213,7 @@ importers:
typescript: ^4.6.3
devDependencies:
'@types/node': 17.0.23
ts-node: 10.7.0_52efxrzidnucw2w35vvobhxasa
ts-node: 10.7.0_ee885bc7281b682b6adbed6ae09ee090
typescript: 4.6.3
packages/client:
@@ -282,6 +282,7 @@ importers:
prettier: ^2.6.2
pretty-bytes: ^6.0.0
react: ^18.0.0
react-countup: ^6.2.0
react-dom: ^18.0.0
react-dropzone: ^12.0.4
react-error-boundary: ^3.1.4
@@ -305,10 +306,10 @@ importers:
dependencies:
'@apollo/client': 3.5.10_react@18.0.0
'@fontsource/inter': 4.5.7
'@headlessui/react': 1.5.0_zpnidt7m3osuk7shl3s4oenomq
'@headlessui/react': 1.5.0_react-dom@18.0.0+react@18.0.0
'@heroicons/react': 1.0.6_react@18.0.0
'@radix-ui/react-dialog': 0.1.7_ca3iilkt5tbo43h35hmeecyxuu
'@radix-ui/react-dropdown-menu': 0.1.6_ca3iilkt5tbo43h35hmeecyxuu
'@radix-ui/react-dialog': 0.1.7_1036842d53ecc2ee6cfbe9d8420b17a5
'@radix-ui/react-dropdown-menu': 0.1.6_1036842d53ecc2ee6cfbe9d8420b17a5
'@radix-ui/react-icons': 1.1.0_react@18.0.0
'@radix-ui/react-progress': 0.1.4_react@18.0.0
'@radix-ui/react-slider': 0.1.4_react@18.0.0
@@ -320,26 +321,27 @@ importers:
byte-size: 8.1.0
clsx: 1.1.1
immer: 9.0.12
jotai: 1.6.2_snqbpbx2kmgyrqporflvt6z4sa
jotai: 1.6.2_93601786fa530d88c1ee895759fb3c90
moment: 2.29.2
phosphor-react: 1.4.1_react@18.0.0
pretty-bytes: 6.0.0
react: 18.0.0
react-countup: 6.2.0_react@18.0.0
react-dom: 18.0.0_react@18.0.0
react-dropzone: 12.0.4_react@18.0.0
react-error-boundary: 3.1.4_react@18.0.0
react-hotkeys-hook: 3.4.4_zpnidt7m3osuk7shl3s4oenomq
react-json-view: 1.21.3_ca3iilkt5tbo43h35hmeecyxuu
react-hotkeys-hook: 3.4.4_react-dom@18.0.0+react@18.0.0
react-json-view: 1.21.3_1036842d53ecc2ee6cfbe9d8420b17a5
react-loading-icons: 1.0.8
react-portal: 4.2.2_zpnidt7m3osuk7shl3s4oenomq
react-query: 3.34.19_zpnidt7m3osuk7shl3s4oenomq
react-portal: 4.2.2_react-dom@18.0.0+react@18.0.0
react-query: 3.34.19_react-dom@18.0.0+react@18.0.0
react-router: 6.3.0_react@18.0.0
react-router-dom: 6.3.0_zpnidt7m3osuk7shl3s4oenomq
react-scrollbars-custom: 4.0.27_zpnidt7m3osuk7shl3s4oenomq
react-router-dom: 6.3.0_react-dom@18.0.0+react@18.0.0
react-scrollbars-custom: 4.0.27_react-dom@18.0.0+react@18.0.0
react-spline: 1.2.1_react@18.0.0
react-transition-group: 4.4.2_zpnidt7m3osuk7shl3s4oenomq
react-transition-group: 4.4.2_react-dom@18.0.0+react@18.0.0
react-virtuoso: 2.9.0_react@18.0.0
rooks: 5.11.0_zpnidt7m3osuk7shl3s4oenomq
rooks: 5.11.0_react-dom@18.0.0+react@18.0.0
tailwindcss: 3.0.23_autoprefixer@10.4.4
zustand: 3.7.2_react@18.0.0
devDependencies:
@@ -859,17 +861,7 @@ packages:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
dev: false
/@headlessui/react/1.5.0_react@18.0.0:
resolution: {integrity: sha512-aaRnYxBb3MU2FNJf3Ut9RMTUqqU3as0aI1lQhgo2n9Fa67wRu14iOGqx93xB+uMNVfNwZ5B3y/Ndm7qZGuFeMQ==}
engines: {node: '>=10'}
peerDependencies:
react: ^16 || ^17 || ^18
react-dom: ^16 || ^17 || ^18
dependencies:
react: 18.0.0
dev: false
/@headlessui/react/1.5.0_zpnidt7m3osuk7shl3s4oenomq:
/@headlessui/react/1.5.0_react-dom@18.0.0+react@18.0.0:
resolution: {integrity: sha512-aaRnYxBb3MU2FNJf3Ut9RMTUqqU3as0aI1lQhgo2n9Fa67wRu14iOGqx93xB+uMNVfNwZ5B3y/Ndm7qZGuFeMQ==}
engines: {node: '>=10'}
peerDependencies:
@@ -880,6 +872,16 @@ packages:
react-dom: 18.0.0_react@18.0.0
dev: false
/@headlessui/react/1.5.0_react@18.0.0:
resolution: {integrity: sha512-aaRnYxBb3MU2FNJf3Ut9RMTUqqU3as0aI1lQhgo2n9Fa67wRu14iOGqx93xB+uMNVfNwZ5B3y/Ndm7qZGuFeMQ==}
engines: {node: '>=10'}
peerDependencies:
react: ^16 || ^17 || ^18
react-dom: ^16 || ^17 || ^18
dependencies:
react: 18.0.0
dev: false
/@heroicons/react/1.0.6_react@18.0.0:
resolution: {integrity: sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==}
peerDependencies:
@@ -995,7 +997,7 @@ packages:
tinycolor2: 1.4.2
dev: true
/@jimp/plugin-contain/0.16.1_hdurtz6n4amigqqh6fpopmgoni:
/@jimp/plugin-contain/0.16.1_38e919e7cde018834207f15ee7b0ce6a:
resolution: {integrity: sha512-44F3dUIjBDHN+Ym/vEfg+jtjMjAqd2uw9nssN67/n4FdpuZUVs7E7wadKY1RRNuJO+WgcD5aDQcsvurXMETQTg==}
peerDependencies:
'@jimp/custom': '>=0.3.5'
@@ -1007,11 +1009,11 @@ packages:
'@jimp/custom': 0.16.1
'@jimp/plugin-blit': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-resize': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-scale': 0.16.1_yobdog5u6yhkf7jbdico3cvr4i
'@jimp/plugin-scale': 0.16.1_c382371bb4f60ea2fd211a04ed8ab1e2
'@jimp/utils': 0.16.1
dev: true
/@jimp/plugin-cover/0.16.1_huj753jw6x6ymgmn4r5xrmstcq:
/@jimp/plugin-cover/0.16.1_3d13feed36f5fd86198de47b78b25314:
resolution: {integrity: sha512-YztWCIldBAVo0zxcQXR+a/uk3/TtYnpKU2CanOPJ7baIuDlWPsG+YE4xTsswZZc12H9Kl7CiziEbDtvF9kwA/Q==}
peerDependencies:
'@jimp/custom': '>=0.3.5'
@@ -1023,7 +1025,7 @@ packages:
'@jimp/custom': 0.16.1
'@jimp/plugin-crop': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-resize': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-scale': 0.16.1_yobdog5u6yhkf7jbdico3cvr4i
'@jimp/plugin-scale': 0.16.1_c382371bb4f60ea2fd211a04ed8ab1e2
'@jimp/utils': 0.16.1
dev: true
@@ -1067,7 +1069,7 @@ packages:
'@jimp/utils': 0.16.1
dev: true
/@jimp/plugin-flip/0.16.1_ebpr3hwfcslp7e7vflcwddbwma:
/@jimp/plugin-flip/0.16.1_205f1d9ec51496ff93f52ac5618c3660:
resolution: {integrity: sha512-KdxTf0zErfZ8DyHkImDTnQBuHby+a5YFdoKI/G3GpBl3qxLBvC+PWkS2F/iN3H7wszP7/TKxTEvWL927pypT0w==}
peerDependencies:
'@jimp/custom': '>=0.3.5'
@@ -1075,7 +1077,7 @@ packages:
dependencies:
'@babel/runtime': 7.17.9
'@jimp/custom': 0.16.1
'@jimp/plugin-rotate': 0.16.1_g72lzg55x5a4ao4td3uebcztme
'@jimp/plugin-rotate': 0.16.1_37f4bc9bbdbf41c03b931ee8408b3361
'@jimp/utils': 0.16.1
dev: true
@@ -1119,7 +1121,7 @@ packages:
'@jimp/utils': 0.16.1
dev: true
/@jimp/plugin-print/0.16.1_kgi3mefd5odb2o5kccttl2e6zq:
/@jimp/plugin-print/0.16.1_5191b610a3eb861d3baa10a735e89ecc:
resolution: {integrity: sha512-ceWgYN40jbN4cWRxixym+csyVymvrryuKBQ+zoIvN5iE6OyS+2d7Mn4zlNgumSczb9GGyZZESIgVcBDA1ezq0Q==}
peerDependencies:
'@jimp/custom': '>=0.3.5'
@@ -1142,7 +1144,7 @@ packages:
'@jimp/utils': 0.16.1
dev: true
/@jimp/plugin-rotate/0.16.1_g72lzg55x5a4ao4td3uebcztme:
/@jimp/plugin-rotate/0.16.1_37f4bc9bbdbf41c03b931ee8408b3361:
resolution: {integrity: sha512-ZUU415gDQ0VjYutmVgAYYxC9Og9ixu2jAGMCU54mSMfuIlmohYfwARQmI7h4QB84M76c9hVLdONWjuo+rip/zg==}
peerDependencies:
'@jimp/custom': '>=0.3.5'
@@ -1158,7 +1160,7 @@ packages:
'@jimp/utils': 0.16.1
dev: true
/@jimp/plugin-scale/0.16.1_yobdog5u6yhkf7jbdico3cvr4i:
/@jimp/plugin-scale/0.16.1_c382371bb4f60ea2fd211a04ed8ab1e2:
resolution: {integrity: sha512-jM2QlgThIDIc4rcyughD5O7sOYezxdafg/2Xtd1csfK3z6fba3asxDwthqPZAgitrLgiKBDp6XfzC07Y/CefUw==}
peerDependencies:
'@jimp/custom': '>=0.3.5'
@@ -1170,7 +1172,7 @@ packages:
'@jimp/utils': 0.16.1
dev: true
/@jimp/plugin-shadow/0.16.1_qjkavnenl6bwqbeu6ngq3leqp4:
/@jimp/plugin-shadow/0.16.1_82540ab48d5f83680494f34d0dac907f:
resolution: {integrity: sha512-MeD2Is17oKzXLnsphAa1sDstTu6nxscugxAEk3ji0GV1FohCvpHBcec0nAq6/czg4WzqfDts+fcPfC79qWmqrA==}
peerDependencies:
'@jimp/custom': '>=0.3.5'
@@ -1184,7 +1186,7 @@ packages:
'@jimp/utils': 0.16.1
dev: true
/@jimp/plugin-threshold/0.16.1_5qgih43agfe2q6apq4cf7aqzei:
/@jimp/plugin-threshold/0.16.1_ec0c83f3603149a8780f87045f821922:
resolution: {integrity: sha512-iGW8U/wiCSR0+6syrPioVGoSzQFt4Z91SsCRbgNKTAk7D+XQv6OI78jvvYg4o0c2FOlwGhqz147HZV5utoSLxA==}
peerDependencies:
'@jimp/custom': '>=0.3.5'
@@ -1209,23 +1211,23 @@ packages:
'@jimp/plugin-blur': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-circle': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-color': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-contain': 0.16.1_hdurtz6n4amigqqh6fpopmgoni
'@jimp/plugin-cover': 0.16.1_huj753jw6x6ymgmn4r5xrmstcq
'@jimp/plugin-contain': 0.16.1_38e919e7cde018834207f15ee7b0ce6a
'@jimp/plugin-cover': 0.16.1_3d13feed36f5fd86198de47b78b25314
'@jimp/plugin-crop': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-displace': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-dither': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-fisheye': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-flip': 0.16.1_ebpr3hwfcslp7e7vflcwddbwma
'@jimp/plugin-flip': 0.16.1_205f1d9ec51496ff93f52ac5618c3660
'@jimp/plugin-gaussian': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-invert': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-mask': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-normalize': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-print': 0.16.1_kgi3mefd5odb2o5kccttl2e6zq
'@jimp/plugin-print': 0.16.1_5191b610a3eb861d3baa10a735e89ecc
'@jimp/plugin-resize': 0.16.1_@jimp+custom@0.16.1
'@jimp/plugin-rotate': 0.16.1_g72lzg55x5a4ao4td3uebcztme
'@jimp/plugin-scale': 0.16.1_yobdog5u6yhkf7jbdico3cvr4i
'@jimp/plugin-shadow': 0.16.1_qjkavnenl6bwqbeu6ngq3leqp4
'@jimp/plugin-threshold': 0.16.1_5qgih43agfe2q6apq4cf7aqzei
'@jimp/plugin-rotate': 0.16.1_37f4bc9bbdbf41c03b931ee8408b3361
'@jimp/plugin-scale': 0.16.1_c382371bb4f60ea2fd211a04ed8ab1e2
'@jimp/plugin-shadow': 0.16.1_82540ab48d5f83680494f34d0dac907f
'@jimp/plugin-threshold': 0.16.1_ec0c83f3603149a8780f87045f821922
timm: 1.7.1
dev: true
@@ -1363,7 +1365,7 @@ packages:
react: 18.0.0
dev: false
/@radix-ui/react-dialog/0.1.7_ca3iilkt5tbo43h35hmeecyxuu:
/@radix-ui/react-dialog/0.1.7_1036842d53ecc2ee6cfbe9d8420b17a5:
resolution: {integrity: sha512-jXt8srGhHBRvEr9jhEAiwwJzWCWZoGRJ030aC9ja/gkRJbZdy0iD3FwXf+Ff4RtsZyLUMHW7VUwFOlz3Ixe1Vw==}
peerDependencies:
react: ^16.8 || ^17.0
@@ -1377,7 +1379,7 @@ packages:
'@radix-ui/react-focus-guards': 0.1.0_react@18.0.0
'@radix-ui/react-focus-scope': 0.1.4_react@18.0.0
'@radix-ui/react-id': 0.1.5_react@18.0.0
'@radix-ui/react-portal': 0.1.4_zpnidt7m3osuk7shl3s4oenomq
'@radix-ui/react-portal': 0.1.4_react-dom@18.0.0+react@18.0.0
'@radix-ui/react-presence': 0.1.2_react@18.0.0
'@radix-ui/react-primitive': 0.1.4_react@18.0.0
'@radix-ui/react-slot': 0.1.2_react@18.0.0
@@ -1385,7 +1387,7 @@ packages:
aria-hidden: 1.1.3
react: 18.0.0
react-dom: 18.0.0_react@18.0.0
react-remove-scroll: 2.4.4_4y5bjn44bzlaofthgjwivvhune
react-remove-scroll: 2.4.4_@types+react@18.0.8+react@18.0.0
transitivePeerDependencies:
- '@types/react'
dev: false
@@ -1405,7 +1407,7 @@ packages:
react: 18.0.0
dev: false
/@radix-ui/react-dropdown-menu/0.1.6_ca3iilkt5tbo43h35hmeecyxuu:
/@radix-ui/react-dropdown-menu/0.1.6_1036842d53ecc2ee6cfbe9d8420b17a5:
resolution: {integrity: sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg==}
peerDependencies:
react: ^16.8 || ^17.0
@@ -1416,7 +1418,7 @@ packages:
'@radix-ui/react-compose-refs': 0.1.0_react@18.0.0
'@radix-ui/react-context': 0.1.1_react@18.0.0
'@radix-ui/react-id': 0.1.5_react@18.0.0
'@radix-ui/react-menu': 0.1.6_ca3iilkt5tbo43h35hmeecyxuu
'@radix-ui/react-menu': 0.1.6_1036842d53ecc2ee6cfbe9d8420b17a5
'@radix-ui/react-primitive': 0.1.4_react@18.0.0
'@radix-ui/react-use-controllable-state': 0.1.0_react@18.0.0
react: 18.0.0
@@ -1464,7 +1466,7 @@ packages:
react: 18.0.0
dev: false
/@radix-ui/react-menu/0.1.6_ca3iilkt5tbo43h35hmeecyxuu:
/@radix-ui/react-menu/0.1.6_1036842d53ecc2ee6cfbe9d8420b17a5:
resolution: {integrity: sha512-ho3+bhpr3oAFkOBJ8VkUb1BcGoiZBB3OmcWPqa6i5RTUKrzNX/d6rauochu2xDlWjiRtpVuiAcsTVOeIC4FbYQ==}
peerDependencies:
react: ^16.8 || ^17.0
@@ -1480,7 +1482,7 @@ packages:
'@radix-ui/react-focus-scope': 0.1.4_react@18.0.0
'@radix-ui/react-id': 0.1.5_react@18.0.0
'@radix-ui/react-popper': 0.1.4_react@18.0.0
'@radix-ui/react-portal': 0.1.4_zpnidt7m3osuk7shl3s4oenomq
'@radix-ui/react-portal': 0.1.4_react-dom@18.0.0+react@18.0.0
'@radix-ui/react-presence': 0.1.2_react@18.0.0
'@radix-ui/react-primitive': 0.1.4_react@18.0.0
'@radix-ui/react-roving-focus': 0.1.5_react@18.0.0
@@ -1489,7 +1491,7 @@ packages:
aria-hidden: 1.1.3
react: 18.0.0
react-dom: 18.0.0_react@18.0.0
react-remove-scroll: 2.4.4_4y5bjn44bzlaofthgjwivvhune
react-remove-scroll: 2.4.4_@types+react@18.0.8+react@18.0.0
transitivePeerDependencies:
- '@types/react'
dev: false
@@ -1511,7 +1513,7 @@ packages:
react: 18.0.0
dev: false
/@radix-ui/react-portal/0.1.4_zpnidt7m3osuk7shl3s4oenomq:
/@radix-ui/react-portal/0.1.4_react-dom@18.0.0+react@18.0.0:
resolution: {integrity: sha512-MO0wRy2eYRTZ/CyOri9NANCAtAtq89DEtg90gicaTlkCfdqCLEBsLb+/q66BZQTr3xX/Vq01nnVfc/TkCqoqvw==}
peerDependencies:
react: ^16.8 || ^17.0
@@ -3213,6 +3215,10 @@ packages:
path-type: 4.0.0
yaml: 1.10.2
/countup.js/2.1.0:
resolution: {integrity: sha512-VanMzLEjkt3Hp/ty5BXikM8s4wE3OH4m1AnFro7THR86nYGRvGfGCoV+zrRJcqTbZi7X1egkLSIeUKDz7+4XLA==}
dev: false
/create-require/1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: true
@@ -5241,7 +5247,7 @@ packages:
regenerator-runtime: 0.13.9
dev: true
/jotai/1.6.2_snqbpbx2kmgyrqporflvt6z4sa:
/jotai/1.6.2_93601786fa530d88c1ee895759fb3c90:
resolution: {integrity: sha512-WglyuXX67f8QPCOX5tDR0N9GhasOV1+IQ3piaF4owr2wBn2CV51T8zbw01ZCme/24JKJilFe6+aA29PJx+jsQw==}
engines: {node: '>=12.7.0'}
peerDependencies:
@@ -5277,7 +5283,7 @@ packages:
dependencies:
immer: 9.0.12
react: 18.0.0
react-query: 3.34.19_zpnidt7m3osuk7shl3s4oenomq
react-query: 3.34.19_react-dom@18.0.0+react@18.0.0
dev: false
/jpeg-js/0.4.2:
@@ -6613,7 +6619,16 @@ packages:
pure-color: 1.3.0
dev: false
/react-device-detect/2.2.2_zpnidt7m3osuk7shl3s4oenomq:
/react-countup/6.2.0_react@18.0.0:
resolution: {integrity: sha512-3WOKAQpWgjyFoH231SHEpIpHhDGb5g5EkTppM6T7vLa3X+8WMdw6750vVcY0wxysKiY00gTFhDwSB5qLU+xPZA==}
peerDependencies:
react: '>= 16.3.0'
dependencies:
countup.js: 2.1.0
react: 18.0.0
dev: false
/react-device-detect/2.2.2_react-dom@18.0.0+react@18.0.0:
resolution: {integrity: sha512-zSN1gIAztUekp5qUT/ybHwQ9fmOqVT1psxpSlTn1pe0CO+fnJHKRLOWWac5nKxOxvOpD/w84hk1I+EydrJp7SA==}
peerDependencies:
react: '>= 0.14.0'
@@ -6634,7 +6649,7 @@ packages:
scheduler: 0.21.0
dev: false
/react-draggable/4.4.5_zpnidt7m3osuk7shl3s4oenomq:
/react-draggable/4.4.5_react-dom@18.0.0+react@18.0.0:
resolution: {integrity: sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==}
peerDependencies:
react: '>= 16.3.0'
@@ -6684,7 +6699,7 @@ packages:
react-side-effect: 2.1.1_react@18.0.0
dev: false
/react-hotkeys-hook/3.4.4_zpnidt7m3osuk7shl3s4oenomq:
/react-hotkeys-hook/3.4.4_react-dom@18.0.0+react@18.0.0:
resolution: {integrity: sha512-vaORq07rWgmuF3owWRhgFV/3VL8/l2q9lz0WyVEddJnWTtKW+AOgU5YgYKuwN6h6h7bCcLG3MFsJIjCrM/5DvQ==}
peerDependencies:
react: '>=16.8.1'
@@ -6699,7 +6714,7 @@ packages:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: false
/react-json-view/1.21.3_ca3iilkt5tbo43h35hmeecyxuu:
/react-json-view/1.21.3_1036842d53ecc2ee6cfbe9d8420b17a5:
resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==}
peerDependencies:
react: ^17.0.0 || ^16.3.0 || ^15.5.4
@@ -6710,7 +6725,7 @@ packages:
react-base16-styling: 0.6.0
react-dom: 18.0.0_react@18.0.0
react-lifecycles-compat: 3.0.4
react-textarea-autosize: 8.3.3_4y5bjn44bzlaofthgjwivvhune
react-textarea-autosize: 8.3.3_@types+react@18.0.8+react@18.0.0
transitivePeerDependencies:
- '@types/react'
- encoding
@@ -6724,7 +6739,7 @@ packages:
resolution: {integrity: sha512-j0myUwDUPoo3qaPkbgnA7U2RNHqLLC+wXcpMWe+rtk3Iw+mGHltZ3QitPSHFKtsFKlpM9UlMmZGZ6sw6WVVW7w==}
dev: false
/react-portal/4.2.2_zpnidt7m3osuk7shl3s4oenomq:
/react-portal/4.2.2_react-dom@18.0.0+react@18.0.0:
resolution: {integrity: sha512-vS18idTmevQxyQpnde0Td6ZcUlv+pD8GTyR42n3CHUQq9OHi1C4jDE4ZWEbEsrbrLRhSECYiao58cvocwMtP7Q==}
peerDependencies:
react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0
@@ -6752,7 +6767,7 @@ packages:
match-sorter: 6.3.1
dev: false
/react-query/3.34.19_zpnidt7m3osuk7shl3s4oenomq:
/react-query/3.34.19_react-dom@18.0.0+react@18.0.0:
resolution: {integrity: sha512-JO0Ymi58WKmvnhgg6bGIrYIeKb64KsKaPWo8JcGnmK2jJxAs2XmMBzlP75ZepSU7CHzcsWtIIyhMrLbX3pb/3w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0
@@ -6775,7 +6790,7 @@ packages:
resolution: {integrity: sha512-suLIhrU2IHKL5JEKR/fAwJv7bbeq4kJ+pJopf77jHwuR+HmJS/HbrPIGsTBUVfw7tXPOmYv7UJ7PCaN49e8x4A==}
engines: {node: '>=0.10.0'}
/react-remove-scroll-bar/2.2.0_4y5bjn44bzlaofthgjwivvhune:
/react-remove-scroll-bar/2.2.0_@types+react@18.0.8+react@18.0.0:
resolution: {integrity: sha512-UU9ZBP1wdMR8qoUs7owiVcpaPwsQxUDC2lypP6mmixaGlARZa7ZIBx1jcuObLdhMOvCsnZcvetOho0wzPa9PYg==}
engines: {node: '>=8.5.0'}
peerDependencies:
@@ -6787,11 +6802,11 @@ packages:
dependencies:
'@types/react': 18.0.8
react: 18.0.0
react-style-singleton: 2.1.1_4y5bjn44bzlaofthgjwivvhune
react-style-singleton: 2.1.1_@types+react@18.0.8+react@18.0.0
tslib: 1.14.1
dev: false
/react-remove-scroll/2.4.4_4y5bjn44bzlaofthgjwivvhune:
/react-remove-scroll/2.4.4_@types+react@18.0.8+react@18.0.0:
resolution: {integrity: sha512-EyC5ohYhaeKbThMSQxuN2i+QC5HqV3AJvNZKEdiATITexu0gHm00+5ko0ltNS1ajYJVeDgVG2baRSCei0AUWlQ==}
engines: {node: '>=8.5.0'}
peerDependencies:
@@ -6803,14 +6818,14 @@ packages:
dependencies:
'@types/react': 18.0.8
react: 18.0.0
react-remove-scroll-bar: 2.2.0_4y5bjn44bzlaofthgjwivvhune
react-style-singleton: 2.1.1_4y5bjn44bzlaofthgjwivvhune
react-remove-scroll-bar: 2.2.0_@types+react@18.0.8+react@18.0.0
react-style-singleton: 2.1.1_@types+react@18.0.8+react@18.0.0
tslib: 1.14.1
use-callback-ref: 1.2.5_4y5bjn44bzlaofthgjwivvhune
use-callback-ref: 1.2.5_@types+react@18.0.8+react@18.0.0
use-sidecar: 1.0.5_react@18.0.0
dev: false
/react-router-dom/6.3.0_zpnidt7m3osuk7shl3s4oenomq:
/react-router-dom/6.3.0_react-dom@18.0.0+react@18.0.0:
resolution: {integrity: sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==}
peerDependencies:
react: '>=16.8'
@@ -6831,14 +6846,14 @@ packages:
react: 18.0.0
dev: false
/react-scrollbars-custom/4.0.27_zpnidt7m3osuk7shl3s4oenomq:
/react-scrollbars-custom/4.0.27_react-dom@18.0.0+react@18.0.0:
resolution: {integrity: sha512-vEIvb+AbxWHtU6yqNfIEtnm06vguFFxDK8UTC6hPMlgEPvrQX7ayoLp9MT+vyszouQ9NgcEtipLmNGEw31pQpQ==}
peerDependencies:
react: '>=16.0.0'
dependencies:
cnbuilder: 2.7.1
react: 18.0.0
react-draggable: 4.4.5_zpnidt7m3osuk7shl3s4oenomq
react-draggable: 4.4.5_react-dom@18.0.0+react@18.0.0
zoom-level: 2.5.0
transitivePeerDependencies:
- react-dom
@@ -6862,7 +6877,7 @@ packages:
three: 0.123.0
dev: false
/react-style-singleton/2.1.1_4y5bjn44bzlaofthgjwivvhune:
/react-style-singleton/2.1.1_@types+react@18.0.8+react@18.0.0:
resolution: {integrity: sha512-jNRp07Jza6CBqdRKNgGhT3u9umWvils1xsuMOjZlghBDH2MU0PL2WZor4PGYjXpnRCa9DQSlHMs/xnABWOwYbA==}
engines: {node: '>=8.5.0'}
peerDependencies:
@@ -6879,7 +6894,7 @@ packages:
tslib: 1.14.1
dev: false
/react-textarea-autosize/8.3.3_4y5bjn44bzlaofthgjwivvhune:
/react-textarea-autosize/8.3.3_@types+react@18.0.8+react@18.0.0:
resolution: {integrity: sha512-2XlHXK2TDxS6vbQaoPbMOfQ8GK7+irc2fVK6QFIcC8GOnH3zI/v481n+j1L0WaPVvKxwesnY93fEfH++sus2rQ==}
engines: {node: '>=10'}
peerDependencies:
@@ -6888,12 +6903,12 @@ packages:
'@babel/runtime': 7.17.9
react: 18.0.0
use-composed-ref: 1.2.1_react@18.0.0
use-latest: 1.2.0_4y5bjn44bzlaofthgjwivvhune
use-latest: 1.2.0_@types+react@18.0.8+react@18.0.0
transitivePeerDependencies:
- '@types/react'
dev: false
/react-transition-group/4.4.2_zpnidt7m3osuk7shl3s4oenomq:
/react-transition-group/4.4.2_react-dom@18.0.0+react@18.0.0:
resolution: {integrity: sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==}
peerDependencies:
react: '>=16.6.0'
@@ -7140,7 +7155,7 @@ packages:
fsevents: 2.3.2
dev: true
/rooks/5.11.0_zpnidt7m3osuk7shl3s4oenomq:
/rooks/5.11.0_react-dom@18.0.0+react@18.0.0:
resolution: {integrity: sha512-acyTuaYbB021hhHAN6uiU6QUUSuGucK9YOU16nOUM0oypqpckYm7nX5v9xnopoX4pNtrxzIM/mtjC8O7fF3/8A==}
engines: {node: '>=10'}
peerDependencies:
@@ -7893,7 +7908,7 @@ packages:
tslib: 2.3.1
dev: false
/ts-node/10.7.0_52efxrzidnucw2w35vvobhxasa:
/ts-node/10.7.0_ee885bc7281b682b6adbed6ae09ee090:
resolution: {integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==}
hasBin: true
peerDependencies:
@@ -8520,7 +8535,7 @@ packages:
engines: {node: '>= 4'}
dev: true
/use-callback-ref/1.2.5_4y5bjn44bzlaofthgjwivvhune:
/use-callback-ref/1.2.5_@types+react@18.0.8+react@18.0.0:
resolution: {integrity: sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==}
engines: {node: '>=8.5.0'}
peerDependencies:
@@ -8542,7 +8557,7 @@ packages:
react: 18.0.0
dev: false
/use-isomorphic-layout-effect/1.1.2_4y5bjn44bzlaofthgjwivvhune:
/use-isomorphic-layout-effect/1.1.2_@types+react@18.0.8+react@18.0.0:
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies:
'@types/react': '*'
@@ -8555,7 +8570,7 @@ packages:
react: 18.0.0
dev: false
/use-latest/1.2.0_4y5bjn44bzlaofthgjwivvhune:
/use-latest/1.2.0_@types+react@18.0.8+react@18.0.0:
resolution: {integrity: sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw==}
peerDependencies:
'@types/react': '*'
@@ -8566,7 +8581,7 @@ packages:
dependencies:
'@types/react': 18.0.8
react: 18.0.0
use-isomorphic-layout-effect: 1.1.2_4y5bjn44bzlaofthgjwivvhune
use-isomorphic-layout-effect: 1.1.2_@types+react@18.0.8+react@18.0.0
dev: false
/use-sidecar/1.0.5_react@18.0.0:
@@ -8733,7 +8748,7 @@ packages:
- supports-color
dev: true
/vite-plugin-tsconfig-paths/1.0.5_w5v33ehmxzy52jcrbupop25zpy:
/vite-plugin-tsconfig-paths/1.0.5_typescript@4.6.3+vite@2.9.5:
resolution: {integrity: sha512-xYGm16eVWqfnlCtxAKQawIq1u+gW//T8hh+EiA17L5i3t+44r40W4G+ZY0TvboK1X3EAKgnjFNxv9dnWnFuAGg==}
peerDependencies:
vite: '*'