diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 5ebb80265..7c5bfc74c 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -63,7 +63,7 @@ "windows": [ { "title": "Spacedrive", - "width": 1200, + "width": 1400, "height": 725, "minWidth": 700, "minHeight": 500, diff --git a/core/bindings/ClientQuery.ts b/core/bindings/ClientQuery.ts index 56e37988c..d48b1859d 100644 --- a/core/bindings/ClientQuery.ts +++ b/core/bindings/ClientQuery.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { LibraryQuery } from "./LibraryQuery"; -export type ClientQuery = { key: "NodeGetLibraries" } | { key: "NodeGetState" } | { key: "SysGetVolumes" } | { key: "JobGetRunning" } | { key: "GetNodes" } | { key: "LibraryQuery", params: { library_id: string, query: LibraryQuery, } }; \ No newline at end of file +export type ClientQuery = { key: "GetLibraries" } | { key: "GetNode" } | { key: "GetVolumes" } | { key: "GetNodes" } | { key: "LibraryQuery", params: { library_id: string, query: LibraryQuery, } }; \ No newline at end of file diff --git a/core/bindings/CoreResource.ts b/core/bindings/CoreResource.ts index 4ed7811b5..7ca5999ca 100644 --- a/core/bindings/CoreResource.ts +++ b/core/bindings/CoreResource.ts @@ -2,5 +2,6 @@ import type { File } from "./File"; import type { JobReport } from "./JobReport"; import type { LocationResource } from "./LocationResource"; +import type { Tag } from "./Tag"; -export type CoreResource = "Client" | "Library" | { Location: LocationResource } | { File: File } | { Job: JobReport } | "Tag"; \ No newline at end of file +export type CoreResource = { key: "Client" } | { key: "Library" } | { key: "Location", data: LocationResource } | { key: "File", data: File } | { key: "Job", data: JobReport } | { key: "Tag", data: Tag }; \ No newline at end of file diff --git a/core/bindings/CoreResponse.ts b/core/bindings/CoreResponse.ts index cabf79dfa..f9ee981c7 100644 --- a/core/bindings/CoreResponse.ts +++ b/core/bindings/CoreResponse.ts @@ -5,6 +5,8 @@ import type { LibraryConfigWrapped } from "./LibraryConfigWrapped"; import type { LocationResource } from "./LocationResource"; import type { NodeState } from "./NodeState"; import type { Statistics } from "./Statistics"; +import type { Tag } from "./Tag"; +import type { TagWithFiles } from "./TagWithFiles"; import type { Volume } from "./Volume"; -export type CoreResponse = { key: "Success", data: null } | { key: "Error", data: string } | { key: "NodeGetLibraries", data: Array } | { key: "SysGetVolumes", data: Array } | { key: "SysGetLocation", data: LocationResource } | { key: "SysGetLocations", data: Array } | { key: "LibGetExplorerDir", data: DirectoryWithContents } | { key: "NodeGetState", data: NodeState } | { key: "LocCreate", data: LocationResource } | { key: "JobGetRunning", data: Array } | { key: "JobGetHistory", data: Array } | { key: "GetLibraryStatistics", data: Statistics }; \ No newline at end of file +export type CoreResponse = { key: "Success", data: null } | { key: "Error", data: string } | { key: "GetLibraries", data: Array } | { key: "GetVolumes", data: Array } | { key: "TagCreateResponse", data: Tag } | { key: "GetTag", data: Tag | null } | { key: "GetTags", data: Array } | { key: "GetLocation", data: LocationResource } | { key: "GetLocations", data: Array } | { key: "GetExplorerDir", data: DirectoryWithContents } | { key: "GetNode", data: NodeState } | { key: "LocCreate", data: LocationResource } | { key: "OpenTag", data: Array } | { key: "GetRunningJobs", data: Array } | { key: "GetJobHistory", data: Array } | { key: "GetLibraryStatistics", data: Statistics }; \ No newline at end of file diff --git a/core/bindings/LibraryCommand.ts b/core/bindings/LibraryCommand.ts index d99033a9f..3438a4085 100644 --- a/core/bindings/LibraryCommand.ts +++ b/core/bindings/LibraryCommand.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type LibraryCommand = { key: "FileReadMetaData", params: { id: number, } } | { key: "FileSetNote", params: { id: number, note: string | null, } } | { key: "FileSetFavorite", params: { id: number, favorite: boolean, } } | { key: "FileDelete", params: { id: number, } } | { key: "TagCreate", params: { name: string, color: string, } } | { key: "TagUpdate", params: { name: string, color: string, } } | { key: "TagAssign", params: { file_id: number, tag_id: number, } } | { key: "TagDelete", params: { id: number, } } | { key: "LocCreate", params: { path: string, } } | { key: "LocUpdate", params: { id: number, name: string | null, } } | { key: "LocDelete", params: { id: number, } } | { key: "LocFullRescan", params: { id: number, } } | { key: "LocQuickRescan", params: { id: number, } } | { key: "SysVolumeUnmount", params: { id: number, } } | { key: "GenerateThumbsForLocation", params: { id: number, path: string, } } | { key: "IdentifyUniqueFiles", params: { id: number, path: string, } }; \ No newline at end of file +export type LibraryCommand = { key: "FileReadMetaData", params: { id: number, } } | { key: "FileSetNote", params: { id: number, note: string | null, } } | { key: "FileSetFavorite", params: { id: number, favorite: boolean, } } | { key: "FileDelete", params: { id: number, } } | { key: "TagCreate", params: { name: string, color: string, } } | { key: "TagUpdate", params: { id: number, name: string | null, color: string | null, } } | { key: "TagAssign", params: { file_id: number, tag_id: number, } } | { key: "TagDelete", params: { id: number, } } | { key: "LocCreate", params: { path: string, } } | { key: "LocUpdate", params: { id: number, name: string | null, } } | { key: "LocDelete", params: { id: number, } } | { key: "LocFullRescan", params: { id: number, } } | { key: "LocQuickRescan", params: { id: number, } } | { key: "VolUnmount", params: { id: number, } } | { key: "GenerateThumbsForLocation", params: { id: number, path: string, } } | { key: "IdentifyUniqueFiles", params: { id: number, path: string, } }; \ No newline at end of file diff --git a/core/bindings/LibraryQuery.ts b/core/bindings/LibraryQuery.ts index 2aa14279c..d90a2fefc 100644 --- a/core/bindings/LibraryQuery.ts +++ b/core/bindings/LibraryQuery.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type LibraryQuery = { key: "LibGetTags" } | { key: "JobGetHistory" } | { key: "SysGetLocations" } | { key: "SysGetLocation", params: { id: number, } } | { key: "LibGetExplorerDir", params: { location_id: number, path: string, limit: number, } } | { key: "GetLibraryStatistics" }; \ No newline at end of file +export type LibraryQuery = { key: "GetJobHistory" } | { key: "GetLocations" } | { key: "GetLocation", params: { id: number, } } | { key: "GetRunningJobs" } | { key: "GetExplorerDir", params: { location_id: number, path: string, limit: number, } } | { key: "GetLibraryStatistics" } | { key: "GetTags" } | { key: "GetFilesTagged", params: { tag_id: number, } }; \ No newline at end of file diff --git a/core/bindings/Tag.ts b/core/bindings/Tag.ts new file mode 100644 index 000000000..60683d4b0 --- /dev/null +++ b/core/bindings/Tag.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface Tag { id: number, pub_id: string, name: string | null, color: string | null, total_files: number | null, redundancy_goal: number | null, date_created: string, date_modified: string, } \ No newline at end of file diff --git a/core/bindings/TagOnFile.ts b/core/bindings/TagOnFile.ts new file mode 100644 index 000000000..97723b51a --- /dev/null +++ b/core/bindings/TagOnFile.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { File } from "./File"; +import type { Tag } from "./Tag"; + +export interface TagOnFile { tag_id: number, tag: Tag | null, file_id: number, file: File | null, date_created: string, } \ No newline at end of file diff --git a/core/bindings/TagWithFiles.ts b/core/bindings/TagWithFiles.ts new file mode 100644 index 000000000..6cf36acae --- /dev/null +++ b/core/bindings/TagWithFiles.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Tag } from "./Tag"; +import type { TagOnFile } from "./TagOnFile"; + +export interface TagWithFiles { tag: Tag, files_with_tag: Array, } \ No newline at end of file diff --git a/core/index.ts b/core/index.ts index 60cc1bc54..22ef6909e 100644 --- a/core/index.ts +++ b/core/index.ts @@ -24,4 +24,7 @@ export * from './bindings/NodeConfig'; export * from './bindings/NodeState'; export * from './bindings/Platform'; export * from './bindings/Statistics'; +export * from './bindings/Tag'; +export * from './bindings/TagOnFile'; +export * from './bindings/TagWithFiles'; export * from './bindings/Volume'; diff --git a/core/prisma/migrations/20220715031021_added_spaces/migration.sql b/core/prisma/migrations/20220715031021_added_spaces/migration.sql new file mode 100644 index 000000000..672e3d89d --- /dev/null +++ b/core/prisma/migrations/20220715031021_added_spaces/migration.sql @@ -0,0 +1,62 @@ +/* + Warnings: + + - You are about to drop the `label_on_files` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `tags_on_files` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "label_on_files"; +PRAGMA foreign_keys=on; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "tags_on_files"; +PRAGMA foreign_keys=on; + +-- CreateTable +CREATE TABLE "tags_on_file" ( + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "tag_id" INTEGER NOT NULL, + "file_id" INTEGER NOT NULL, + + PRIMARY KEY ("tag_id", "file_id"), + CONSTRAINT "tags_on_file_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "tags_on_file_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "tags" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateTable +CREATE TABLE "label_on_file" ( + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "label_id" INTEGER NOT NULL, + "file_id" INTEGER NOT NULL, + + PRIMARY KEY ("label_id", "file_id"), + CONSTRAINT "label_on_file_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "label_on_file_label_id_fkey" FOREIGN KEY ("label_id") REFERENCES "labels" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateTable +CREATE TABLE "spaces" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pub_id" TEXT NOT NULL, + "name" TEXT, + "description" TEXT, + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "file_in_space" ( + "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "space_id" INTEGER NOT NULL, + "file_id" INTEGER NOT NULL, + + PRIMARY KEY ("space_id", "file_id"), + CONSTRAINT "file_in_space_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "file_in_space_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "spaces" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION +); + +-- CreateIndex +CREATE UNIQUE INDEX "spaces_pub_id_key" ON "spaces"("pub_id"); diff --git a/core/prisma/migrations/20220716023638_tags_color/migration.sql b/core/prisma/migrations/20220716023638_tags_color/migration.sql new file mode 100644 index 000000000..72d3fb7b9 --- /dev/null +++ b/core/prisma/migrations/20220716023638_tags_color/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "tags" ADD COLUMN "color" TEXT; diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index 130151f62..47a0da0c1 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -133,6 +133,7 @@ model File { tags TagOnFile[] labels LabelOnFile[] albums FileInAlbum[] + spaces FileInSpace[] paths FilePath[] comments Comment[] media_data MediaData? @@ -227,6 +228,7 @@ model Tag { id Int @id @default(autoincrement()) pub_id String @unique name String? + color String? total_files Int? @default(0) redundancy_goal Int? @default(1) date_created DateTime @default(now()) @@ -246,7 +248,7 @@ model TagOnFile { file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction) @@id([tag_id, file_id]) - @@map("tags_on_files") + @@map("tags_on_file") } model Label { @@ -270,7 +272,32 @@ model LabelOnFile { file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction) @@id([label_id, file_id]) - @@map("label_on_files") + @@map("label_on_file") +} + +model Space { + id Int @id @default(autoincrement()) + pub_id String @unique + name String? + description String? + date_created DateTime @default(now()) + date_modified DateTime @default(now()) + + files FileInSpace[] + @@map("spaces") +} + +model FileInSpace { + date_created DateTime @default(now()) + + space_id Int + space Space @relation(fields: [space_id], references: [id], onDelete: NoAction, onUpdate: NoAction) + + file_id Int + file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction) + + @@id([space_id, file_id]) + @@map("file_in_space") } model Job { diff --git a/core/src/file/explorer/open.rs b/core/src/file/explorer/open.rs index 8afe1c9d8..d4e5f484d 100644 --- a/core/src/file/explorer/open.rs +++ b/core/src/file/explorer/open.rs @@ -2,8 +2,9 @@ use crate::{ encode::THUMBNAIL_CACHE_DIR_NAME, file::{DirectoryWithContents, FileError, FilePath}, library::LibraryContext, - prisma::file_path, + prisma::{file_path, tag, tag_on_file}, sys::get_location, + tag::{Tag, TagError, TagOnFile, TagWithFiles}, }; use std::path::Path; @@ -60,3 +61,29 @@ pub async fn open_dir( contents: file_paths, }) } + +pub async fn open_tag(ctx: &LibraryContext, tag_id: i32) -> Result { + let tag: Tag = ctx + .db + .tag() + .find_unique(tag::id::equals(tag_id)) + .exec() + .await? + .ok_or_else(|| TagError::TagNotFound(tag_id))? + .into(); + + let files_with_tag: Vec = ctx + .db + .tag_on_file() + .find_many(vec![tag_on_file::tag_id::equals(tag_id)]) + .exec() + .await? + .into_iter() + .map(Into::into) + .collect(); + + Ok(TagWithFiles { + tag, + files_with_tag, + }) +} diff --git a/core/src/file/mod.rs b/core/src/file/mod.rs index 780633b93..5bfa9423e 100644 --- a/core/src/file/mod.rs +++ b/core/src/file/mod.rs @@ -158,7 +158,7 @@ pub async fn set_note( ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery { library_id: ctx.id.to_string(), - query: LibraryQuery::LibGetExplorerDir { + query: LibraryQuery::GetExplorerDir { limit: 0, path: "".to_string(), location_id: 0, @@ -185,7 +185,7 @@ pub async fn favorite( ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery { library_id: ctx.id.to_string(), - query: LibraryQuery::LibGetExplorerDir { + query: LibraryQuery::GetExplorerDir { limit: 0, path: "".to_string(), location_id: 0, diff --git a/core/src/job/worker.rs b/core/src/job/worker.rs index 29c0ec17e..e0893e2ad 100644 --- a/core/src/job/worker.rs +++ b/core/src/job/worker.rs @@ -3,6 +3,7 @@ use super::{ Job, JobManager, }; use crate::{library::LibraryContext, ClientQuery, CoreEvent, LibraryQuery}; + use std::{sync::Arc, time::Duration}; use tokio::{ sync::{ @@ -172,7 +173,10 @@ impl Worker { } } ctx.emit(CoreEvent::InvalidateQueryDebounced( - ClientQuery::JobGetRunning, + ClientQuery::LibraryQuery { + library_id: ctx.id.to_string(), + query: LibraryQuery::GetRunningJobs, + }, )) .await; } @@ -180,12 +184,15 @@ impl Worker { worker.job_report.status = JobStatus::Completed; worker.job_report.update(&ctx).await.unwrap_or(()); - ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::JobGetRunning)) - .await; + ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery { + library_id: ctx.id.to_string(), + query: LibraryQuery::GetRunningJobs, + })) + .await; ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery { library_id: ctx.id.to_string(), - query: LibraryQuery::JobGetHistory, + query: LibraryQuery::GetJobHistory, })) .await; break; @@ -196,9 +203,10 @@ impl Worker { ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery { library_id: ctx.id.to_string(), - query: LibraryQuery::JobGetHistory, + query: LibraryQuery::GetJobHistory, })) .await; + break; } } diff --git a/core/src/lib.rs b/core/src/lib.rs index f865b766b..29dfecd2e 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -8,6 +8,8 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use tag::{Tag, TagWithFiles}; + use thiserror::Error; use tokio::sync::{ mpsc::{self, unbounded_channel, UnboundedReceiver, UnboundedSender}, @@ -24,6 +26,7 @@ mod library; mod node; mod prisma; mod sys; +mod tag; mod util; // a wrapper around external input with a returning sender channel for core to respond @@ -235,15 +238,18 @@ impl Node { CoreResponse::Success(()) } // CRUD for tags - LibraryCommand::TagCreate { name: _, color: _ } => todo!(), - LibraryCommand::TagAssign { - file_id: _, - tag_id: _, - } => todo!(), - LibraryCommand::TagUpdate { name: _, color: _ } => todo!(), - LibraryCommand::TagDelete { id: _ } => todo!(), + LibraryCommand::TagCreate { name, color } => { + tag::create_tag(ctx, name, color).await? + } + LibraryCommand::TagAssign { file_id, tag_id } => { + tag::tag_assign(ctx, file_id, tag_id).await? + } + LibraryCommand::TagDelete { id } => tag::tag_delete(ctx, id).await?, + LibraryCommand::TagUpdate { id, name, color } => { + tag::update_tag(ctx, id, name, color).await? + } // CRUD for libraries - LibraryCommand::SysVolumeUnmount { id: _ } => todo!(), + LibraryCommand::VolUnmount { id: _ } => todo!(), LibraryCommand::GenerateThumbsForLocation { id, path } => { ctx.spawn_job(Box::new(ThumbnailJob { location_id: id, @@ -269,18 +275,15 @@ impl Node { // query sources of data async fn exec_query(&self, query: ClientQuery) -> Result { Ok(match query { - ClientQuery::NodeGetLibraries => CoreResponse::NodeGetLibraries( - self.library_manager.get_all_libraries_config().await, - ), - ClientQuery::NodeGetState => CoreResponse::NodeGetState(NodeState { + ClientQuery::GetLibraries => { + CoreResponse::GetLibraries(self.library_manager.get_all_libraries_config().await) + } + ClientQuery::GetNode => CoreResponse::GetNode(NodeState { config: self.config.get().await, data_path: self.config.data_directory().to_str().unwrap().to_string(), }), - ClientQuery::SysGetVolumes => CoreResponse::SysGetVolumes(sys::Volume::get_volumes()?), - ClientQuery::JobGetRunning => { - CoreResponse::JobGetRunning(self.jobs.get_running().await) - } ClientQuery::GetNodes => todo!(), + ClientQuery::GetVolumes => CoreResponse::GetVolumes(sys::Volume::get_volumes()?), ClientQuery::LibraryQuery { library_id, query } => { let ctx = match self.library_manager.get_ctx(library_id.clone()).await { Some(ctx) => ctx, @@ -290,28 +293,34 @@ impl Node { } }; match query { - LibraryQuery::SysGetLocations => { - CoreResponse::SysGetLocations(sys::get_locations(&ctx).await?) + LibraryQuery::GetLocations => { + CoreResponse::GetLocations(sys::get_locations(&ctx).await?) + } + LibraryQuery::GetRunningJobs => { + CoreResponse::GetRunningJobs(self.jobs.get_running().await) } // get location from library - LibraryQuery::SysGetLocation { id } => { - CoreResponse::SysGetLocation(sys::get_location(&ctx, id).await?) + LibraryQuery::GetLocation { id } => { + CoreResponse::GetLocation(sys::get_location(&ctx, id).await?) } // return contents of a directory for the explorer - LibraryQuery::LibGetExplorerDir { + LibraryQuery::GetExplorerDir { path, location_id, limit: _, - } => CoreResponse::LibGetExplorerDir( + } => CoreResponse::GetExplorerDir( file::explorer::open_dir(&ctx, &location_id, &path).await?, ), - LibraryQuery::LibGetTags => todo!(), - LibraryQuery::JobGetHistory => { - CoreResponse::JobGetHistory(JobManager::get_history(&ctx).await?) + LibraryQuery::GetJobHistory => { + CoreResponse::GetJobHistory(JobManager::get_history(&ctx).await?) } LibraryQuery::GetLibraryStatistics => CoreResponse::GetLibraryStatistics( library::Statistics::calculate(&ctx).await?, ), + LibraryQuery::GetTags => tag::get_all_tags(ctx).await?, + LibraryQuery::GetFilesTagged { tag_id } => { + tag::get_files_for_tag(ctx, tag_id).await? + } } } }) @@ -347,27 +356,68 @@ pub enum ClientCommand { #[ts(export)] pub enum LibraryCommand { // Files - FileReadMetaData { id: i32 }, - FileSetNote { id: i32, note: Option }, - FileSetFavorite { id: i32, favorite: bool }, + FileReadMetaData { + id: i32, + }, + FileSetNote { + id: i32, + note: Option, + }, + FileSetFavorite { + id: i32, + favorite: bool, + }, // FileEncrypt { id: i32, algorithm: EncryptionAlgorithm }, - FileDelete { id: i32 }, + FileDelete { + id: i32, + }, // Tags - TagCreate { name: String, color: String }, - TagUpdate { name: String, color: String }, - TagAssign { file_id: i32, tag_id: i32 }, - TagDelete { id: i32 }, + TagCreate { + name: String, + color: String, + }, + TagUpdate { + id: i32, + name: Option, + color: Option, + }, + TagAssign { + file_id: i32, + tag_id: i32, + }, + TagDelete { + id: i32, + }, // Locations - LocCreate { path: String }, - LocUpdate { id: i32, name: Option }, - LocDelete { id: i32 }, - LocFullRescan { id: i32 }, - LocQuickRescan { id: i32 }, + LocCreate { + path: String, + }, + LocUpdate { + id: i32, + name: Option, + }, + LocDelete { + id: i32, + }, + LocFullRescan { + id: i32, + }, + LocQuickRescan { + id: i32, + }, // System - SysVolumeUnmount { id: i32 }, - GenerateThumbsForLocation { id: i32, path: String }, + VolUnmount { + id: i32, + }, + GenerateThumbsForLocation { + id: i32, + path: String, + }, // PurgeDatabase, - IdentifyUniqueFiles { id: i32, path: String }, + IdentifyUniqueFiles { + id: i32, + path: String, + }, } /// is a query destined for the core @@ -375,10 +425,9 @@ pub enum LibraryCommand { #[serde(tag = "key", content = "params")] #[ts(export)] pub enum ClientQuery { - NodeGetLibraries, - NodeGetState, - SysGetVolumes, - JobGetRunning, + GetLibraries, + GetNode, + GetVolumes, GetNodes, LibraryQuery { library_id: String, @@ -391,18 +440,22 @@ pub enum ClientQuery { #[serde(tag = "key", content = "params")] #[ts(export)] pub enum LibraryQuery { - LibGetTags, - JobGetHistory, - SysGetLocations, - SysGetLocation { + GetJobHistory, + GetLocations, + GetLocation { id: i32, }, - LibGetExplorerDir { + GetRunningJobs, + GetExplorerDir { location_id: i32, path: String, limit: i32, }, GetLibraryStatistics, + GetTags, + GetFilesTagged { + tag_id: i32, + }, } // represents an event this library can emit @@ -433,15 +486,19 @@ pub struct NodeState { pub enum CoreResponse { Success(()), Error(String), - NodeGetLibraries(Vec), - SysGetVolumes(Vec), - SysGetLocation(sys::LocationResource), - SysGetLocations(Vec), - LibGetExplorerDir(file::DirectoryWithContents), - NodeGetState(NodeState), + GetLibraries(Vec), + GetVolumes(Vec), + TagCreateResponse(tag::Tag), + GetTag(Option), + GetTags(Vec), + GetLocation(sys::LocationResource), + GetLocations(Vec), + GetExplorerDir(file::DirectoryWithContents), + GetNode(NodeState), LocCreate(sys::LocationResource), - JobGetRunning(Vec), - JobGetHistory(Vec), + OpenTag(Vec), + GetRunningJobs(Vec), + GetJobHistory(Vec), GetLibraryStatistics(library::Statistics), } @@ -462,6 +519,7 @@ pub enum CoreError { } #[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[serde(tag = "key", content = "data")] #[ts(export)] pub enum CoreResource { Client, @@ -469,5 +527,5 @@ pub enum CoreResource { Location(sys::LocationResource), File(file::File), Job(JobReport), - Tag, + Tag(tag::Tag), } diff --git a/core/src/library/library_manager.rs b/core/src/library/library_manager.rs index 5c1aca635..45e9a2acd 100644 --- a/core/src/library/library_manager.rs +++ b/core/src/library/library_manager.rs @@ -138,7 +138,7 @@ impl LibraryManager { self.libraries.write().await.push(library); self.node_context - .emit(CoreEvent::InvalidateQuery(ClientQuery::NodeGetLibraries)) + .emit(CoreEvent::InvalidateQuery(ClientQuery::GetLibraries)) .await; Ok(()) @@ -184,7 +184,7 @@ impl LibraryManager { .await?; self.node_context - .emit(CoreEvent::InvalidateQuery(ClientQuery::NodeGetLibraries)) + .emit(CoreEvent::InvalidateQuery(ClientQuery::GetLibraries)) .await; Ok(()) } @@ -209,7 +209,7 @@ impl LibraryManager { libraries.retain(|l| l.id != id); self.node_context - .emit(CoreEvent::InvalidateQuery(ClientQuery::NodeGetLibraries)) + .emit(CoreEvent::InvalidateQuery(ClientQuery::GetLibraries)) .await; Ok(()) } diff --git a/core/src/sys/locations.rs b/core/src/sys/locations.rs index 220bacecb..13ed06fbb 100644 --- a/core/src/sys/locations.rs +++ b/core/src/sys/locations.rs @@ -212,7 +212,7 @@ pub async fn create_location( Err(e) => Err(LocationError::DotfileWriteFailure(e, path.to_string()))?, } - // ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::SysGetLocations)) + // ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::GetLocations)) // .await; location @@ -239,7 +239,7 @@ pub async fn delete_location(ctx: &LibraryContext, location_id: i32) -> Result<( ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery { library_id: ctx.id.to_string(), - query: LibraryQuery::SysGetLocations, + query: LibraryQuery::GetLocations, })) .await; diff --git a/core/src/tag/mod.rs b/core/src/tag/mod.rs new file mode 100644 index 000000000..61d343333 --- /dev/null +++ b/core/src/tag/mod.rs @@ -0,0 +1,188 @@ +use crate::{ + file::File, + library::LibraryContext, + prisma::{ + self, file, + tag::{self}, + tag_on_file, + }, + ClientQuery, CoreError, CoreEvent, CoreResponse, LibraryQuery, +}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use ts_rs::TS; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct Tag { + pub id: i32, + pub pub_id: String, + pub name: Option, + pub color: Option, + + pub total_files: Option, + pub redundancy_goal: Option, + + pub date_created: chrono::DateTime, + pub date_modified: chrono::DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct TagOnFile { + pub tag_id: i32, + pub tag: Option, + + pub file_id: i32, + pub file: Option, + + pub date_created: chrono::DateTime, +} + +impl Into for tag::Data { + fn into(self) -> Tag { + Tag { + id: self.id, + pub_id: self.pub_id, + name: self.name, + color: self.color, + total_files: self.total_files, + redundancy_goal: self.redundancy_goal, + date_created: self.date_created.into(), + date_modified: self.date_modified.into(), + } + } +} + +impl Into for tag_on_file::Data { + fn into(self) -> TagOnFile { + TagOnFile { + tag_id: self.tag_id, + tag: self.tag.map(|t| (*t).into()), + file_id: self.file_id, + file: self.file.map(|f| (*f).into()), + date_created: self.date_created.into(), + } + } +} + +#[derive(Serialize, Deserialize, TS, Debug)] +#[ts(export)] +pub struct TagWithFiles { + pub tag: Tag, + pub files_with_tag: Vec, +} + +#[derive(Error, Debug)] +pub enum TagError { + #[error("Tag not found")] + TagNotFound(i32), + #[error("Database error")] + DatabaseError(#[from] prisma::QueryError), +} + +pub async fn create_tag( + ctx: LibraryContext, + name: String, + color: String, +) -> Result { + let created_tag = ctx + .db + .tag() + .create( + tag::pub_id::set(uuid::Uuid::new_v4().to_string()), + vec![tag::name::set(Some(name)), tag::color::set(Some(color))], + ) + .exec() + .await + .unwrap(); + + ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery { + library_id: ctx.id.to_string(), + query: LibraryQuery::GetTags, + })) + .await; + + Ok(CoreResponse::TagCreateResponse(created_tag.into())) +} + +pub async fn update_tag( + ctx: LibraryContext, + id: i32, + name: Option, + color: Option, +) -> Result { + ctx.db + .tag() + .find_unique(tag::id::equals(id)) + .update(vec![tag::name::set(name), tag::color::set(color)]) + .exec() + .await + .unwrap(); + + ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery { + library_id: ctx.id.to_string(), + query: LibraryQuery::GetTags, + })) + .await; + + Ok(CoreResponse::Success(())) +} + +pub async fn tag_assign( + ctx: LibraryContext, + file_id: i32, + tag_id: i32, +) -> Result { + ctx.db.tag_on_file().create( + tag_on_file::tag::link(tag::UniqueWhereParam::IdEquals(tag_id)), + tag_on_file::file::link(file::UniqueWhereParam::IdEquals(file_id)), + vec![], + ); + + Ok(CoreResponse::Success(())) +} + +pub async fn tag_delete(ctx: LibraryContext, id: i32) -> Result { + ctx.db + .tag() + .find_unique(tag::id::equals(id)) + .delete() + .exec() + .await? + .unwrap(); + + ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery { + library_id: ctx.id.to_string(), + query: LibraryQuery::GetTags, + })) + .await; + + Ok(CoreResponse::Success(())) +} + +pub async fn get_files_for_tag(ctx: LibraryContext, id: i32) -> Result { + let tag: Option = ctx + .db + .tag() + .find_unique(tag::id::equals(id)) + .exec() + .await? + .map(Into::into); + + Ok(CoreResponse::GetTag(tag)) +} + +pub async fn get_all_tags(ctx: LibraryContext) -> Result { + let tags: Vec = ctx + .db + .tag() + .find_many(vec![]) + .exec() + .await? + .into_iter() + .map(Into::into) + .collect(); + + Ok(CoreResponse::GetTags(tags)) +} diff --git a/packages/client/src/stores/useLibraryStore.ts b/packages/client/src/stores/useLibraryStore.ts index 53a8a4e55..fecbf871d 100644 --- a/packages/client/src/stores/useLibraryStore.ts +++ b/packages/client/src/stores/useLibraryStore.ts @@ -51,7 +51,7 @@ export const useLibraryStore = create()( // is memorized and can be used safely in any component export const useCurrentLibrary = () => { const { currentLibraryUuid, switchLibrary } = useLibraryStore(); - const { data: libraries } = useBridgeQuery('NodeGetLibraries', undefined, {}); + const { data: libraries } = useBridgeQuery('GetLibraries', undefined, {}); // memorize library to avoid re-running find function const currentLibrary = useMemo(() => { diff --git a/packages/interface/package.json b/packages/interface/package.json index 02e05a74d..709290266 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -38,10 +38,12 @@ "phosphor-react": "^1.4.1", "pretty-bytes": "^6.0.0", "react": "^18.1.0", + "react-colorful": "^5.5.1", "react-countup": "^6.2.0", "react-dom": "^18.1.0", "react-dropzone": "^14.2.1", "react-error-boundary": "^3.1.4", + "react-hook-form": "^7.31.3", "react-hotkeys-hook": "^3.4.6", "react-json-view": "^1.21.3", "react-loading-icons": "^1.1.0", diff --git a/packages/interface/src/App.tsx b/packages/interface/src/App.tsx index a04b4382f..c9f672640 100644 --- a/packages/interface/src/App.tsx +++ b/packages/interface/src/App.tsx @@ -16,7 +16,7 @@ const queryClient = new QueryClient(); function RouterContainer(props: { props: AppProps }) { useCoreEvents(); const [appProps, setAppProps] = useState(props.props); - const { data: client } = useBridgeQuery('NodeGetState'); + const { data: client } = useBridgeQuery('GetNode'); useEffect(() => { setAppProps({ diff --git a/packages/interface/src/AppRouter.tsx b/packages/interface/src/AppRouter.tsx index 640d3f1eb..644864922 100644 --- a/packages/interface/src/AppRouter.tsx +++ b/packages/interface/src/AppRouter.tsx @@ -17,6 +17,10 @@ import AppearanceSettings from './screens/settings/client/AppearanceSettings'; import ExtensionSettings from './screens/settings/client/ExtensionsSettings'; import GeneralSettings from './screens/settings/client/GeneralSettings'; import KeybindSettings from './screens/settings/client/KeybindSettings'; +import PrivacySettings from './screens/settings/client/PrivacySettings'; +import AboutSpacedrive from './screens/settings/info/AboutSpacedrive'; +import Changelog from './screens/settings/info/Changelog'; +import Support from './screens/settings/info/Support'; import ContactsSettings from './screens/settings/library/ContactsSettings'; import KeysSettings from './screens/settings/library/KeysSetting'; import LibraryGeneralSettings from './screens/settings/library/LibraryGeneralSettings'; @@ -34,7 +38,7 @@ export function AppRouter() { let location = useLocation(); let state = location.state as { backgroundLocation?: Location }; const libraryState = useLibraryStore(); - const { data: libraries } = useBridgeQuery('NodeGetLibraries'); + const { data: libraries } = useBridgeQuery('GetLibraries'); // TODO: This can be removed once we add a setup flow to the app useEffect(() => { @@ -79,6 +83,10 @@ export function AppRouter() { } /> } /> } /> + } /> + } /> + } /> + } /> } /> } /> diff --git a/packages/interface/src/components/file/FileList.tsx b/packages/interface/src/components/file/FileList.tsx index e2a792a60..403c6afe8 100644 --- a/packages/interface/src/components/file/FileList.tsx +++ b/packages/interface/src/components/file/FileList.tsx @@ -21,7 +21,7 @@ interface IColumn { const PADDING_SIZE = 130; -// Function ensure no types are loss, but guarantees that they are Column[] +// Function ensure no types are lost, but guarantees that they are Column[] function ensureIsColumns(data: T) { return data; } @@ -48,20 +48,19 @@ const GridItemContainer = styled.div` `; export const FileList: React.FC<{ location_id: number; path: string; limit: number }> = (props) => { + const path = props.path; const size = useWindowSize(); const tableContainer = useRef(null); const VList = useRef(null); - const { data: client } = useBridgeQuery('NodeGetState', undefined, { + const { data: client } = useBridgeQuery('GetNode', undefined, { refetchOnWindowFocus: false }); - const path = props.path; - const { selectedRowIndex, setSelectedRowIndex, setLocationId, layoutMode } = useExplorerStore(); const [goingUp, setGoingUp] = useState(false); - const { data: currentDir } = useLibraryQuery('LibGetExplorerDir', { + const { data: currentDir } = useLibraryQuery('GetExplorerDir', { location_id: props.location_id, path, limit: props.limit @@ -124,11 +123,7 @@ export const FileList: React.FC<{ location_id: number; path: string; limit: numb ); return ( -
+
diff --git a/packages/interface/src/components/file/Sidebar.tsx b/packages/interface/src/components/file/Sidebar.tsx index 153d755b9..ca4052c0e 100644 --- a/packages/interface/src/components/file/Sidebar.tsx +++ b/packages/interface/src/components/file/Sidebar.tsx @@ -22,7 +22,7 @@ export const SidebarLink = (props: NavLinkProps & { children: React.ReactNode }) {({ isActive }) => ( = (props) => { const appProps = useContext(AppPropsContext); - const { data: locationsResponse, isError: isLocationsError } = useLibraryQuery('SysGetLocations'); + const { data: locationsResponse, isError: isLocationsError } = useLibraryQuery('GetLocations'); let locations = Array.isArray(locationsResponse) ? locationsResponse : []; @@ -96,13 +96,7 @@ export const Sidebar: React.FC = (props) => { const { mutate: createLocation } = useLibraryCommand('LocCreate'); - const tags = [ - { id: 1, name: 'Keepsafe', color: '#FF6788' }, - { id: 2, name: 'OBS', color: '#BF88FF' }, - { id: 3, name: 'BlackMagic', color: '#F0C94A' }, - { id: 4, name: 'Camera Roll', color: '#00F0DB' }, - { id: 5, name: 'Spacedrive', color: '#00F079' } - ]; + const { data: tags } = useLibraryQuery('GetTags'); return (
= (props) => { Photos - - {isExperimental ? ( - - - Debug - - ) : ( - <> - )}
Locations @@ -256,11 +241,11 @@ export const Sidebar: React.FC = (props) => {
Tags
- {tags.map((tag, index) => ( + {tags?.slice(0, 6).map((tag, index) => (
{tag.name} diff --git a/packages/interface/src/components/jobs/RunningJobsWidget.tsx b/packages/interface/src/components/jobs/RunningJobsWidget.tsx index 85152f4f0..0ef77eccf 100644 --- a/packages/interface/src/components/jobs/RunningJobsWidget.tsx +++ b/packages/interface/src/components/jobs/RunningJobsWidget.tsx @@ -51,7 +51,7 @@ const MiddleTruncatedText = ({ }; export default function RunningJobsWidget() { - const { data: jobs } = useBridgeQuery('JobGetRunning'); + const { data: jobs } = useBridgeQuery('GetRunningJobs'); return (
diff --git a/packages/interface/src/components/layout/TopBar.tsx b/packages/interface/src/components/layout/TopBar.tsx index beb32a322..d5df60bd1 100644 --- a/packages/interface/src/components/layout/TopBar.tsx +++ b/packages/interface/src/components/layout/TopBar.tsx @@ -31,19 +31,27 @@ export interface TopBarButtonProps } interface SearchBarProps extends DefaultProps {} -const TopBarButton: React.FC = ({ icon: Icon, ...props }) => { +const TopBarButton: React.FC = ({ + icon: Icon, + left, + right, + group, + active, + className, + ...props +}) => { return (
diff --git a/packages/interface/src/components/primitive/PopoverPicker.tsx b/packages/interface/src/components/primitive/PopoverPicker.tsx new file mode 100644 index 000000000..7ba176126 --- /dev/null +++ b/packages/interface/src/components/primitive/PopoverPicker.tsx @@ -0,0 +1,40 @@ +import clsx from 'clsx'; +import React, { useCallback, useRef, useState } from 'react'; +import { HexColorPicker } from 'react-colorful'; + +import useClickOutside from '../../hooks/useClickOutside'; + +interface PopoverPickerProps { + value: string; + onChange: (color: string) => void; + className?: string; +} + +export const PopoverPicker = ({ value, onChange, className }: PopoverPickerProps) => { + const popover = useRef(null); + const [isOpen, toggle] = useState(false); + + const close = useCallback(() => toggle(false), []); + useClickOutside(popover, close); + + return ( +
+
toggle(true)} + /> + {/* Pick Color */} + + {isOpen && ( +
+ +
+ )} +
+ ); +}; diff --git a/packages/interface/src/hooks/useClickOutside.ts b/packages/interface/src/hooks/useClickOutside.ts new file mode 100644 index 000000000..96aa121f4 --- /dev/null +++ b/packages/interface/src/hooks/useClickOutside.ts @@ -0,0 +1,35 @@ +import { useEffect } from 'react'; + +// Improved version of https://usehooks.com/useOnClickOutside/ +const useClickOutside = (ref: any, handler: any) => { + useEffect(() => { + let startedInside = false; + let startedWhenMounted = false; + + const listener = (event: any) => { + // Do nothing if `mousedown` or `touchstart` started inside ref element + if (startedInside || !startedWhenMounted) return; + // Do nothing if clicking ref's element or descendent elements + if (!ref.current || ref.current.contains(event.target)) return; + + handler(event); + }; + + const validateEventStart = (event: any) => { + startedWhenMounted = ref.current; + startedInside = ref.current && ref.current.contains(event.target); + }; + + document.addEventListener('mousedown', validateEventStart); + document.addEventListener('touchstart', validateEventStart); + document.addEventListener('click', listener); + + return () => { + document.removeEventListener('mousedown', validateEventStart); + document.removeEventListener('touchstart', validateEventStart); + document.removeEventListener('click', listener); + }; + }, [ref, handler]); +}; + +export default useClickOutside; diff --git a/packages/interface/src/screens/Debug.tsx b/packages/interface/src/screens/Debug.tsx index d0758facb..254a1b90a 100644 --- a/packages/interface/src/screens/Debug.tsx +++ b/packages/interface/src/screens/Debug.tsx @@ -7,10 +7,10 @@ import CodeBlock from '../components/primitive/Codeblock'; export const DebugScreen: React.FC<{}> = (props) => { const appPropsContext = useContext(AppPropsContext); - const { data: nodeState } = useBridgeQuery('NodeGetState'); - const { data: libraryState } = useBridgeQuery('NodeGetLibraries'); - const { data: jobs } = useBridgeQuery('JobGetRunning'); - const { data: jobHistory } = useLibraryQuery('JobGetHistory'); + const { data: nodeState } = useBridgeQuery('GetNode'); + const { data: libraryState } = useBridgeQuery('GetLibraries'); + const { data: jobs } = useBridgeQuery('GetRunningJobs'); + const { data: jobHistory } = useLibraryQuery('GetJobHistory'); // const { mutate: purgeDB } = useBridgeCommand('PurgeDatabase', { // onMutate: () => { // alert('Database purged'); diff --git a/packages/interface/src/screens/Explorer.tsx b/packages/interface/src/screens/Explorer.tsx index 5ef7f4025..d0db3468d 100644 --- a/packages/interface/src/screens/Explorer.tsx +++ b/packages/interface/src/screens/Explorer.tsx @@ -19,17 +19,17 @@ export const ExplorerScreen: React.FC<{}> = () => { const { selectedRowIndex } = useExplorerStore(); // Current Location - const { data: currentLocation } = useLibraryQuery('SysGetLocation', { id: location_id }); + const { data: currentLocation } = useLibraryQuery('GetLocation', { id: location_id }); // Current Directory const { data: currentDir } = useLibraryQuery( - 'LibGetExplorerDir', + 'GetExplorerDir', { location_id: location_id!, path, limit }, { enabled: !!location_id } ); return ( -
+
diff --git a/packages/interface/src/screens/Overview.tsx b/packages/interface/src/screens/Overview.tsx index 15378c486..d065d3673 100644 --- a/packages/interface/src/screens/Overview.tsx +++ b/packages/interface/src/screens/Overview.tsx @@ -102,7 +102,7 @@ const StatItem: React.FC = (props) => { export const OverviewScreen = () => { const { data: libraryStatistics, isLoading: isStatisticsLoading } = useLibraryQuery('GetLibraryStatistics'); - const { data: nodeState } = useBridgeQuery('NodeGetState'); + const { data: nodeState } = useBridgeQuery('GetNode'); const { overviewStats, setOverviewStats } = useOverviewState(); diff --git a/packages/interface/src/screens/settings/Settings.tsx b/packages/interface/src/screens/settings/Settings.tsx index 0bcafc3f0..6a5aaec6d 100644 --- a/packages/interface/src/screens/settings/Settings.tsx +++ b/packages/interface/src/screens/settings/Settings.tsx @@ -1,4 +1,5 @@ import { + AnnotationIcon, CogIcon, CollectionIcon, DatabaseIcon, @@ -8,12 +9,17 @@ import { KeyIcon, LibraryIcon, LightBulbIcon, + LockClosedIcon, + ShieldCheckIcon, + SparklesIcon, TagIcon, TerminalIcon } from '@heroicons/react/outline'; import { BookOpen, Cloud, + FlyingSaucer, + HandWaving, HardDrive, Hash, Info, @@ -21,6 +27,7 @@ import { PaintBrush, PuzzlePiece, ShareNetwork, + Shield, UsersFour } from 'phosphor-react'; import React from 'react'; @@ -44,6 +51,10 @@ export const SettingsScreen: React.FC = () => { Libraries + + + Privacy + Appearance @@ -86,7 +97,7 @@ export const SettingsScreen: React.FC = () => { Sync */} - Advanced + {/* Advanced Networking @@ -94,15 +105,15 @@ export const SettingsScreen: React.FC = () => { Developer - + */} Resources - + About - + Changelog diff --git a/packages/interface/src/screens/settings/client/ExtensionsSettings.tsx b/packages/interface/src/screens/settings/client/ExtensionsSettings.tsx index 01c535a13..9a2645e37 100644 --- a/packages/interface/src/screens/settings/client/ExtensionsSettings.tsx +++ b/packages/interface/src/screens/settings/client/ExtensionsSettings.tsx @@ -58,7 +58,7 @@ function ExtensionItem(props: { extension: ExtensionItemData }) { } export default function ExtensionSettings() { - // const { data: volumes } = useBridgeQuery('SysGetVolumes'); + // const { data: volumes } = useBridgeQuery('GetVolumes'); return ( diff --git a/packages/interface/src/screens/settings/client/GeneralSettings.tsx b/packages/interface/src/screens/settings/client/GeneralSettings.tsx index 66550f7c8..3c42dda37 100644 --- a/packages/interface/src/screens/settings/client/GeneralSettings.tsx +++ b/packages/interface/src/screens/settings/client/GeneralSettings.tsx @@ -1,10 +1,15 @@ +import { useBridgeQuery } from '@sd/client'; +import { Input } from '@sd/ui'; +import { Database } from 'phosphor-react'; import React from 'react'; +import Card from '../../../components/layout/Card'; +import { Toggle } from '../../../components/primitive'; import { SettingsContainer } from '../../../components/settings/SettingsContainer'; import { SettingsHeader } from '../../../components/settings/SettingsHeader'; export default function GeneralSettings() { - // const { data: volumes } = useBridgeQuery('SysGetVolumes'); + const { data: node } = useBridgeQuery('GetNode'); return ( @@ -12,24 +17,53 @@ export default function GeneralSettings() { title="General Settings" description="General settings related to this client." /> - {/* -
-
- { - const name = volume.name && volume.name.length ? volume.name : volume.mount_point; - return { - key: name, - option: name, - description: volume.mount_point - }; - }) ?? [] - } - /> + +
+
+ Connected Node +
+
+ 0 Peers + + Running + +
+
+ +
+
+
+ + Node Name + + +
+
+ + Node Port + + +
+
+ + Node ID + + +
+
+
+ + Run daemon when app closed +
+
+ + + Data Folder + {node?.data_path} +
- */} + ); } diff --git a/packages/interface/src/screens/settings/client/PrivacySettings.tsx b/packages/interface/src/screens/settings/client/PrivacySettings.tsx new file mode 100644 index 000000000..b2e1e6475 --- /dev/null +++ b/packages/interface/src/screens/settings/client/PrivacySettings.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Toggle } from '../../../components/primitive'; +import { InputContainer } from '../../../components/primitive/InputContainer'; +import { SettingsContainer } from '../../../components/settings/SettingsContainer'; +import { SettingsHeader } from '../../../components/settings/SettingsHeader'; + +export default function PrivacySettings() { + return ( + + + + ); +} diff --git a/packages/interface/src/screens/settings/info/AboutSpacedrive.tsx b/packages/interface/src/screens/settings/info/AboutSpacedrive.tsx new file mode 100644 index 000000000..209b10705 --- /dev/null +++ b/packages/interface/src/screens/settings/info/AboutSpacedrive.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import { Toggle } from '../../../components/primitive'; +import { InputContainer } from '../../../components/primitive/InputContainer'; +import { SettingsContainer } from '../../../components/settings/SettingsContainer'; +import { SettingsHeader } from '../../../components/settings/SettingsHeader'; + +export default function AboutSpacedrive() { + return ( + + + Version {} +
+ Created by + + Jamie Pine, Brendan Allan, Oscar Beaumont, Haden Fletcher, Haris Mehrzad Benjamin Akar, + and many more. + +
+
+ ); +} diff --git a/packages/interface/src/screens/settings/info/Changelog.tsx b/packages/interface/src/screens/settings/info/Changelog.tsx new file mode 100644 index 000000000..be5375016 --- /dev/null +++ b/packages/interface/src/screens/settings/info/Changelog.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Toggle } from '../../../components/primitive'; +import { InputContainer } from '../../../components/primitive/InputContainer'; +import { SettingsContainer } from '../../../components/settings/SettingsContainer'; +import { SettingsHeader } from '../../../components/settings/SettingsHeader'; + +export default function Changelog() { + return ( + + + + ); +} diff --git a/packages/interface/src/screens/settings/info/Support.tsx b/packages/interface/src/screens/settings/info/Support.tsx new file mode 100644 index 000000000..bff11a308 --- /dev/null +++ b/packages/interface/src/screens/settings/info/Support.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Toggle } from '../../../components/primitive'; +import { InputContainer } from '../../../components/primitive/InputContainer'; +import { SettingsContainer } from '../../../components/settings/SettingsContainer'; +import { SettingsHeader } from '../../../components/settings/SettingsHeader'; + +export default function Support() { + return ( + + + + ); +} diff --git a/packages/interface/src/screens/settings/library/LibraryGeneralSettings.tsx b/packages/interface/src/screens/settings/library/LibraryGeneralSettings.tsx index 9d45d2f3e..917364378 100644 --- a/packages/interface/src/screens/settings/library/LibraryGeneralSettings.tsx +++ b/packages/interface/src/screens/settings/library/LibraryGeneralSettings.tsx @@ -64,7 +64,7 @@ export default function LibraryGeneralSettings() { />
- Name + Name setName(e.target.value)} @@ -72,7 +72,9 @@ export default function LibraryGeneralSettings() { />
- Description + + Description + setDescription(e.target.value)} diff --git a/packages/interface/src/screens/settings/library/LocationSettings.tsx b/packages/interface/src/screens/settings/library/LocationSettings.tsx index 3b6c67c26..ed1a131f6 100644 --- a/packages/interface/src/screens/settings/library/LocationSettings.tsx +++ b/packages/interface/src/screens/settings/library/LocationSettings.tsx @@ -16,7 +16,7 @@ import { SettingsHeader } from '../../../components/settings/SettingsHeader'; // ]; export default function LocationSettings() { - const { data: locations } = useLibraryQuery('SysGetLocations'); + const { data: locations } = useLibraryQuery('GetLocations'); const appProps = useContext(AppPropsContext); diff --git a/packages/interface/src/screens/settings/library/TagsSettings.tsx b/packages/interface/src/screens/settings/library/TagsSettings.tsx index 19bb977f6..ce7eaf9ba 100644 --- a/packages/interface/src/screens/settings/library/TagsSettings.tsx +++ b/packages/interface/src/screens/settings/library/TagsSettings.tsx @@ -1,12 +1,186 @@ -import React from 'react'; +import { TrashIcon } from '@heroicons/react/outline'; +import { useLibraryCommand, useLibraryQuery } from '@sd/client'; +import { Button, Input } from '@sd/ui'; +import clsx from 'clsx'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { useDebounce } from 'rooks'; +import Card from '../../../components/layout/Card'; +import Dialog from '../../../components/layout/Dialog'; +import { Toggle } from '../../../components/primitive'; +import { InputContainer } from '../../../components/primitive/InputContainer'; +import { PopoverPicker } from '../../../components/primitive/PopoverPicker'; import { SettingsContainer } from '../../../components/settings/SettingsContainer'; import { SettingsHeader } from '../../../components/settings/SettingsHeader'; export default function TagsSettings() { + const [openCreateModal, setOpenCreateModal] = useState(false); + // creating new tag state + const [newColor, setNewColor] = useState('#A717D9'); + const [newName, setNewName] = useState(''); + + const { data: tags } = useLibraryQuery('GetTags'); + + const [selectedTag, setSelectedTag] = useState(null); + + const currentTag = useMemo(() => { + return tags?.find((t) => t.id === selectedTag); + }, [tags, selectedTag]); + + const { mutate: createTag, isLoading } = useLibraryCommand('TagCreate', { + onError: (e) => { + console.log('error', e); + }, + onSuccess: (data) => { + setOpenCreateModal(false); + } + }); + + const { mutate: updateTag, isLoading: tagUpdateLoading } = useLibraryCommand('TagUpdate'); + + const { mutate: deleteTag, isLoading: tagDeleteLoading } = useLibraryCommand('TagDelete'); + + // set default selected tag + useEffect(() => { + if (!currentTag && tags?.length) { + setSelectedTag(tags[0].id); + } + }, [tags]); + + useEffect(() => { + reset(currentTag); + }, [currentTag]); + + const { register, handleSubmit, watch, reset, control } = useForm({ defaultValues: currentTag }); + + const submitTagUpdate = handleSubmit((data) => updateTag(data)); + + const autoUpdateTag = useCallback(useDebounce(submitTagUpdate, 500), []); + + useEffect(() => { + const subscription = watch(() => autoUpdateTag()); + return () => subscription.unsubscribe(); + }); + return ( - + + { + createTag({ + name: newName, + color: newColor + }); + }} + loading={isLoading} + ctaLabel="Create" + trigger={ + + } + > +
+ + setNewName(e.target.value)} + className="w-full pl-[40px]" + placeholder="Name" + /> +
+
+
+ } + /> + + +
+ {tags?.map((tag) => ( +
setSelectedTag(tag.id === selectedTag ? null : tag.id)} + key={tag.id} + className={clsx( + 'flex items-center rounded px-1.5 py-0.5', + selectedTag == tag.id && 'ring' + )} + style={{ backgroundColor: tag.color + 'CC' }} + > + {tag.name} +
+ ))} +
+
+ {currentTag ? ( +
+
+
+ + Color + +
+ ( + + )} + /> + + +
+
+
+ + Name + + +
+
+ { + deleteTag({ id: currentTag.id }); + }} + loading={tagDeleteLoading} + ctaDanger + ctaLabel="Delete" + trigger={ + + } + /> +
+ + + +
+ ) : ( +
No Tag Selected
+ )} ); } diff --git a/packages/interface/src/screens/settings/node/LibrariesSettings.tsx b/packages/interface/src/screens/settings/node/LibrariesSettings.tsx index 5c12f8c3e..1f05c1d09 100644 --- a/packages/interface/src/screens/settings/node/LibrariesSettings.tsx +++ b/packages/interface/src/screens/settings/node/LibrariesSettings.tsx @@ -65,7 +65,7 @@ export default function LibrarySettings() { } }); - const { data: libraries } = useBridgeQuery('NodeGetLibraries'); + const { data: libraries } = useBridgeQuery('GetLibraries'); function createNewLib() { if (newLibName) { diff --git a/packages/interface/src/style.scss b/packages/interface/src/style.scss index e58d884fb..ef44fb5b1 100644 --- a/packages/interface/src/style.scss +++ b/packages/interface/src/style.scss @@ -130,3 +130,24 @@ body { .dialog-content[data-state='closed'] { animation: bounceDown 100ms ease-in forwards; } + +.picker { + position: relative; +} + +.swatch { + width: 28px; + height: 28px; + border-radius: 8px; + border: 3px solid #fff; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(0, 0, 0, 0.1); + cursor: pointer; +} + +.popover { + position: absolute; + top: calc(100% + 2px); + left: 0; + border-radius: 9px; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb35be3d4..84e1d945b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -298,10 +298,12 @@ importers: prettier: ^2.6.2 pretty-bytes: ^6.0.0 react: ^18.1.0 + react-colorful: ^5.5.1 react-countup: ^6.2.0 react-dom: ^18.1.0 react-dropzone: ^14.2.1 react-error-boundary: ^3.1.4 + react-hook-form: ^7.31.3 react-hotkeys-hook: ^3.4.6 react-json-view: ^1.21.3 react-loading-icons: ^1.1.0 @@ -347,10 +349,12 @@ importers: phosphor-react: 1.4.1_react@18.1.0 pretty-bytes: 6.0.0 react: 18.1.0 + react-colorful: 5.5.1_ef5jwxihqo6n7gxfmzogljlgcm react-countup: 6.2.0_react@18.1.0 react-dom: 18.1.0_react@18.1.0 react-dropzone: 14.2.1_react@18.1.0 react-error-boundary: 3.1.4_react@18.1.0 + react-hook-form: 7.32.0_react@18.1.0 react-hotkeys-hook: 3.4.6_ef5jwxihqo6n7gxfmzogljlgcm react-json-view: 1.21.3_ohobp6rpsmerwlq5ipwfh5yigy react-loading-icons: 1.1.0 @@ -13910,6 +13914,16 @@ packages: react: 18.1.0 dev: false + /react-colorful/5.5.1_ef5jwxihqo6n7gxfmzogljlgcm: + resolution: {integrity: sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.1.0 + react-dom: 18.1.0_react@18.1.0 + dev: false + /react-countup/6.2.0_react@18.1.0: resolution: {integrity: sha512-3WOKAQpWgjyFoH231SHEpIpHhDGb5g5EkTppM6T7vLa3X+8WMdw6750vVcY0wxysKiY00gTFhDwSB5qLU+xPZA==} peerDependencies: