mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-08 06:02:49 +00:00
Bunch 'O Features (#336)
* CRUD for tags. * Implement tags query in open.rs and make some changes to CRUD. * Tag update. * Hopefully working get tags. * added node config + spaces schema * add missing routes * begin tag ui * renaming query names to better fit convention * tags progress * tag edit * fix delete tag description Co-authored-by: xPolar <polar@polar.blue>
This commit is contained in:
parent
77bf17e2be
commit
9961c49759
|
@ -63,7 +63,7 @@
|
|||
"windows": [
|
||||
{
|
||||
"title": "Spacedrive",
|
||||
"width": 1200,
|
||||
"width": 1400,
|
||||
"height": 725,
|
||||
"minWidth": 700,
|
||||
"minHeight": 500,
|
||||
|
|
|
@ -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, } };
|
||||
export type ClientQuery = { key: "GetLibraries" } | { key: "GetNode" } | { key: "GetVolumes" } | { key: "GetNodes" } | { key: "LibraryQuery", params: { library_id: string, query: LibraryQuery, } };
|
|
@ -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";
|
||||
export type CoreResource = { key: "Client" } | { key: "Library" } | { key: "Location", data: LocationResource } | { key: "File", data: File } | { key: "Job", data: JobReport } | { key: "Tag", data: Tag };
|
|
@ -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<LibraryConfigWrapped> } | { key: "SysGetVolumes", data: Array<Volume> } | { key: "SysGetLocation", data: LocationResource } | { key: "SysGetLocations", data: Array<LocationResource> } | { key: "LibGetExplorerDir", data: DirectoryWithContents } | { key: "NodeGetState", data: NodeState } | { key: "LocCreate", data: LocationResource } | { key: "JobGetRunning", data: Array<JobReport> } | { key: "JobGetHistory", data: Array<JobReport> } | { key: "GetLibraryStatistics", data: Statistics };
|
||||
export type CoreResponse = { key: "Success", data: null } | { key: "Error", data: string } | { key: "GetLibraries", data: Array<LibraryConfigWrapped> } | { key: "GetVolumes", data: Array<Volume> } | { key: "TagCreateResponse", data: Tag } | { key: "GetTag", data: Tag | null } | { key: "GetTags", data: Array<Tag> } | { key: "GetLocation", data: LocationResource } | { key: "GetLocations", data: Array<LocationResource> } | { key: "GetExplorerDir", data: DirectoryWithContents } | { key: "GetNode", data: NodeState } | { key: "LocCreate", data: LocationResource } | { key: "OpenTag", data: Array<TagWithFiles> } | { key: "GetRunningJobs", data: Array<JobReport> } | { key: "GetJobHistory", data: Array<JobReport> } | { key: "GetLibraryStatistics", data: Statistics };
|
|
@ -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, } };
|
||||
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, } };
|
|
@ -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" };
|
||||
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, } };
|
3
core/bindings/Tag.ts
Normal file
3
core/bindings/Tag.ts
Normal file
|
@ -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, }
|
5
core/bindings/TagOnFile.ts
Normal file
5
core/bindings/TagOnFile.ts
Normal file
|
@ -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, }
|
5
core/bindings/TagWithFiles.ts
Normal file
5
core/bindings/TagWithFiles.ts
Normal file
|
@ -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<TagOnFile>, }
|
|
@ -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';
|
||||
|
|
|
@ -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");
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "tags" ADD COLUMN "color" TEXT;
|
|
@ -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 {
|
||||
|
|
|
@ -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<TagWithFiles, TagError> {
|
||||
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<TagOnFile> = 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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
176
core/src/lib.rs
176
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<CoreResponse, CoreError> {
|
||||
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<String> },
|
||||
FileSetFavorite { id: i32, favorite: bool },
|
||||
FileReadMetaData {
|
||||
id: i32,
|
||||
},
|
||||
FileSetNote {
|
||||
id: i32,
|
||||
note: Option<String>,
|
||||
},
|
||||
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<String>,
|
||||
color: Option<String>,
|
||||
},
|
||||
TagAssign {
|
||||
file_id: i32,
|
||||
tag_id: i32,
|
||||
},
|
||||
TagDelete {
|
||||
id: i32,
|
||||
},
|
||||
// Locations
|
||||
LocCreate { path: String },
|
||||
LocUpdate { id: i32, name: Option<String> },
|
||||
LocDelete { id: i32 },
|
||||
LocFullRescan { id: i32 },
|
||||
LocQuickRescan { id: i32 },
|
||||
LocCreate {
|
||||
path: String,
|
||||
},
|
||||
LocUpdate {
|
||||
id: i32,
|
||||
name: Option<String>,
|
||||
},
|
||||
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<LibraryConfigWrapped>),
|
||||
SysGetVolumes(Vec<sys::Volume>),
|
||||
SysGetLocation(sys::LocationResource),
|
||||
SysGetLocations(Vec<sys::LocationResource>),
|
||||
LibGetExplorerDir(file::DirectoryWithContents),
|
||||
NodeGetState(NodeState),
|
||||
GetLibraries(Vec<LibraryConfigWrapped>),
|
||||
GetVolumes(Vec<sys::Volume>),
|
||||
TagCreateResponse(tag::Tag),
|
||||
GetTag(Option<tag::Tag>),
|
||||
GetTags(Vec<tag::Tag>),
|
||||
GetLocation(sys::LocationResource),
|
||||
GetLocations(Vec<sys::LocationResource>),
|
||||
GetExplorerDir(file::DirectoryWithContents),
|
||||
GetNode(NodeState),
|
||||
LocCreate(sys::LocationResource),
|
||||
JobGetRunning(Vec<JobReport>),
|
||||
JobGetHistory(Vec<JobReport>),
|
||||
OpenTag(Vec<tag::TagWithFiles>),
|
||||
GetRunningJobs(Vec<JobReport>),
|
||||
GetJobHistory(Vec<JobReport>),
|
||||
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),
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
188
core/src/tag/mod.rs
Normal file
188
core/src/tag/mod.rs
Normal file
|
@ -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<String>,
|
||||
pub color: Option<String>,
|
||||
|
||||
pub total_files: Option<i32>,
|
||||
pub redundancy_goal: Option<i32>,
|
||||
|
||||
pub date_created: chrono::DateTime<chrono::Utc>,
|
||||
pub date_modified: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct TagOnFile {
|
||||
pub tag_id: i32,
|
||||
pub tag: Option<Tag>,
|
||||
|
||||
pub file_id: i32,
|
||||
pub file: Option<File>,
|
||||
|
||||
pub date_created: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl Into<Tag> 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<TagOnFile> 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<TagOnFile>,
|
||||
}
|
||||
|
||||
#[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<CoreResponse, CoreError> {
|
||||
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<String>,
|
||||
color: Option<String>,
|
||||
) -> Result<CoreResponse, CoreError> {
|
||||
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<CoreResponse, CoreError> {
|
||||
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<CoreResponse, CoreError> {
|
||||
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<CoreResponse, CoreError> {
|
||||
let tag: Option<Tag> = 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<CoreResponse, CoreError> {
|
||||
let tags: Vec<Tag> = ctx
|
||||
.db
|
||||
.tag()
|
||||
.find_many(vec![])
|
||||
.exec()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
Ok(CoreResponse::GetTags(tags))
|
||||
}
|
|
@ -51,7 +51,7 @@ export const useLibraryStore = create<LibraryStore>()(
|
|||
// 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(() => {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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() {
|
|||
<Route path="tags" element={<TagsSettings />} />
|
||||
<Route path="nodes" element={<NodesSettings />} />
|
||||
<Route path="keys" element={<KeysSettings />} />
|
||||
<Route path="privacy" element={<PrivacySettings />} />
|
||||
<Route path="about" element={<AboutSpacedrive />} />
|
||||
<Route path="changelog" element={<Changelog />} />
|
||||
<Route path="support" element={<Support />} />
|
||||
</Route>
|
||||
<Route path="explorer/:id" element={<ExplorerScreen />} />
|
||||
<Route path="tag/:id" element={<TagScreen />} />
|
||||
|
|
|
@ -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<T extends IColumn[]>(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 | HTMLDivElement>(null);
|
||||
const VList = useRef<null | VirtuosoHandle>(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 (
|
||||
<div
|
||||
ref={tableContainer}
|
||||
style={{ marginTop: -44 }}
|
||||
className="w-full pl-2 bg-white cursor-default dark:bg-gray-600"
|
||||
>
|
||||
<div ref={tableContainer} style={{ marginTop: -44 }} className="w-full pl-2 cursor-default ">
|
||||
<LocationContext.Provider
|
||||
value={{ location_id: props.location_id, data_path: client?.data_path as string }}
|
||||
>
|
||||
|
|
|
@ -22,7 +22,7 @@ export const SidebarLink = (props: NavLinkProps & { children: React.ReactNode })
|
|||
{({ isActive }) => (
|
||||
<span
|
||||
className={clsx(
|
||||
'max-w mb-[2px] text-gray-550 dark:text-gray-150 rounded px-2 py-1 flex flex-row flex-grow items-center font-medium text-sm',
|
||||
'max-w mb-[2px] text-gray-550 dark:text-gray-300 rounded px-2 py-1 flex flex-row flex-grow items-center font-medium text-sm',
|
||||
{
|
||||
'!bg-primary !text-white hover:bg-primary dark:hover:bg-primary': isActive
|
||||
},
|
||||
|
@ -81,7 +81,7 @@ export const Sidebar: React.FC<SidebarProps> = (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<SidebarProps> = (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 (
|
||||
<div
|
||||
|
@ -192,15 +186,6 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
|||
<Icon component={PhotographIcon} />
|
||||
Photos
|
||||
</SidebarLink>
|
||||
|
||||
{isExperimental ? (
|
||||
<SidebarLink to="debug">
|
||||
<Icon component={Code} />
|
||||
Debug
|
||||
</SidebarLink>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Heading>Locations</Heading>
|
||||
|
@ -256,11 +241,11 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
|||
<div>
|
||||
<Heading>Tags</Heading>
|
||||
<div className="mb-2">
|
||||
{tags.map((tag, index) => (
|
||||
{tags?.slice(0, 6).map((tag, index) => (
|
||||
<SidebarLink key={index} to={`tag/${tag.id}`} className="">
|
||||
<div
|
||||
className="w-[12px] h-[12px] rounded-full"
|
||||
style={{ backgroundColor: tag.color }}
|
||||
style={{ backgroundColor: tag.color || '#efefef' }}
|
||||
/>
|
||||
<span className="ml-2 text-sm">{tag.name}</span>
|
||||
</SidebarLink>
|
||||
|
|
|
@ -51,7 +51,7 @@ const MiddleTruncatedText = ({
|
|||
};
|
||||
|
||||
export default function RunningJobsWidget() {
|
||||
const { data: jobs } = useBridgeQuery('JobGetRunning');
|
||||
const { data: jobs } = useBridgeQuery('GetRunningJobs');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
|
|
|
@ -31,19 +31,27 @@ export interface TopBarButtonProps
|
|||
}
|
||||
interface SearchBarProps extends DefaultProps {}
|
||||
|
||||
const TopBarButton: React.FC<TopBarButtonProps> = ({ icon: Icon, ...props }) => {
|
||||
const TopBarButton: React.FC<TopBarButtonProps> = ({
|
||||
icon: Icon,
|
||||
left,
|
||||
right,
|
||||
group,
|
||||
active,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={clsx(
|
||||
'mr-[1px] py-0.5 px-0.5 text-md font-medium hover:bg-gray-150 dark:transparent dark:hover:bg-gray-550 rounded-md transition-colors duration-100',
|
||||
{
|
||||
'rounded-r-none rounded-l-none': props.group && !props.left && !props.right,
|
||||
'rounded-r-none': props.group && props.left,
|
||||
'rounded-l-none': props.group && props.right,
|
||||
'dark:bg-gray-550': props.active
|
||||
'rounded-r-none rounded-l-none': group && !left && !right,
|
||||
'rounded-r-none': group && left,
|
||||
'rounded-l-none': group && right,
|
||||
'dark:bg-gray-550': active
|
||||
},
|
||||
props.className
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Icon weight={'regular'} className="m-0.5 w-5 h-5 text-gray-450 dark:text-gray-150" />
|
||||
|
|
|
@ -17,7 +17,7 @@ export const InputContainer: React.FC<InputContainerProps> = (props) => {
|
|||
className={clsx('flex flex-col w-full', !props.mini && 'pb-6', props.className)}
|
||||
{...props}
|
||||
>
|
||||
<h3 className="mb-1 font-medium text-gray-700 dark:text-gray-100">{props.title}</h3>
|
||||
<h3 className="mb-1 text-sm font-medium text-gray-700 dark:text-gray-100">{props.title}</h3>
|
||||
{!!props.description && <p className="mb-2 text-sm text-gray-400 ">{props.description}</p>}
|
||||
{!props.mini && props.children}
|
||||
</div>
|
||||
|
|
|
@ -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<HTMLDivElement | null>(null);
|
||||
const [isOpen, toggle] = useState(false);
|
||||
|
||||
const close = useCallback(() => toggle(false), []);
|
||||
useClickOutside(popover, close);
|
||||
|
||||
return (
|
||||
<div className={clsx('relative flex items-center mt-3', className)}>
|
||||
<div
|
||||
className={clsx('w-5 h-5 rounded-full shadow ', isOpen && 'dark:border-gray-500')}
|
||||
style={{ backgroundColor: value }}
|
||||
onClick={() => toggle(true)}
|
||||
/>
|
||||
{/* <span className="inline ml-2 text-sm text-gray-200">Pick Color</span> */}
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
style={{ top: 'calc(100% + 7px)' }}
|
||||
className="absolute left-0 rounded-md shadow"
|
||||
ref={popover}
|
||||
>
|
||||
<HexColorPicker color={value} onChange={onChange} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
35
packages/interface/src/hooks/useClickOutside.ts
Normal file
35
packages/interface/src/hooks/useClickOutside.ts
Normal file
|
@ -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;
|
|
@ -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');
|
||||
|
|
|
@ -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 (
|
||||
<div className="relative flex flex-col w-full bg-gray-600">
|
||||
<div className="relative flex flex-col w-full bg-gray-650">
|
||||
<TopBar />
|
||||
<div className="relative flex flex-row w-full max-h-full">
|
||||
<FileList location_id={location_id} path={path} limit={limit} />
|
||||
|
|
|
@ -102,7 +102,7 @@ const StatItem: React.FC<StatItemProps> = (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();
|
||||
|
||||
|
|
|
@ -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 = () => {
|
|||
<SettingsIcon component={CollectionIcon} />
|
||||
Libraries
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/privacy">
|
||||
<SettingsIcon component={ShieldCheckIcon} />
|
||||
Privacy
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/appearance">
|
||||
<SettingsIcon component={PaintBrush} />
|
||||
Appearance
|
||||
|
@ -86,7 +97,7 @@ export const SettingsScreen: React.FC = () => {
|
|||
<SettingsIcon component={ShareNetwork} />
|
||||
Sync
|
||||
</SidebarLink> */}
|
||||
<SettingsHeading>Advanced</SettingsHeading>
|
||||
{/* <SettingsHeading>Advanced</SettingsHeading>
|
||||
<SidebarLink to="/settings/p2p">
|
||||
<SettingsIcon component={ShareNetwork} />
|
||||
Networking
|
||||
|
@ -94,15 +105,15 @@ export const SettingsScreen: React.FC = () => {
|
|||
<SidebarLink to="/settings/experimental">
|
||||
<SettingsIcon component={TerminalIcon} />
|
||||
Developer
|
||||
</SidebarLink>
|
||||
</SidebarLink> */}
|
||||
|
||||
<SettingsHeading>Resources</SettingsHeading>
|
||||
<SidebarLink to="/settings/about">
|
||||
<SettingsIcon component={BookOpen} />
|
||||
<SettingsIcon component={FlyingSaucer} />
|
||||
About
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/changelog">
|
||||
<SettingsIcon component={LightBulbIcon} />
|
||||
<SettingsIcon component={AnnotationIcon} />
|
||||
Changelog
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/settings/support">
|
||||
|
|
|
@ -58,7 +58,7 @@ function ExtensionItem(props: { extension: ExtensionItemData }) {
|
|||
}
|
||||
|
||||
export default function ExtensionSettings() {
|
||||
// const { data: volumes } = useBridgeQuery('SysGetVolumes');
|
||||
// const { data: volumes } = useBridgeQuery('GetVolumes');
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
|
|
|
@ -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 (
|
||||
<SettingsContainer>
|
||||
|
@ -12,24 +17,53 @@ export default function GeneralSettings() {
|
|||
title="General Settings"
|
||||
description="General settings related to this client."
|
||||
/>
|
||||
{/* <InputContainer title="Volumes" description="A list of volumes running on this device.">
|
||||
<div className="flex flex-row space-x-2">
|
||||
<div className="flex flex-grow">
|
||||
<Listbox
|
||||
options={
|
||||
volumes?.map((volume) => {
|
||||
const name = volume.name && volume.name.length ? volume.name : volume.mount_point;
|
||||
return {
|
||||
key: name,
|
||||
option: name,
|
||||
description: volume.mount_point
|
||||
};
|
||||
}) ?? []
|
||||
}
|
||||
/>
|
||||
<Card className="px-5 dark:bg-gray-600">
|
||||
<div className="flex flex-col w-full my-2">
|
||||
<div className="flex">
|
||||
<span className="font-semibold">Connected Node</span>
|
||||
<div className="flex-grow" />
|
||||
<div className="space-x-2">
|
||||
<span className="px-2 py-[2px] rounded text-xs font-medium bg-gray-500">0 Peers</span>
|
||||
<span className="px-1.5 py-[2px] rounded text-xs font-medium bg-primary-600">
|
||||
Running
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="mt-2 mb-4 border-gray-500 " />
|
||||
<div className="flex flex-row space-x-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="mb-1 text-xs font-medium text-gray-700 dark:text-gray-100">
|
||||
Node Name
|
||||
</span>
|
||||
<Input value={node?.name} />
|
||||
</div>
|
||||
<div className="flex flex-col w-[100px]">
|
||||
<span className="mb-1 text-xs font-medium text-gray-700 dark:text-gray-100">
|
||||
Node Port
|
||||
</span>
|
||||
<Input contentEditable={false} value={node?.p2p_port || 5795} />
|
||||
</div>
|
||||
<div className="flex flex-col w-[295px]">
|
||||
<span className="mb-1 text-xs font-medium text-gray-700 dark:text-gray-100">
|
||||
Node ID
|
||||
</span>
|
||||
<Input contentEditable={false} value={node?.id} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center mt-5 space-x-3">
|
||||
<Toggle size="sm" value />
|
||||
<span className="text-sm text-gray-200">Run daemon when app closed</span>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-400">
|
||||
<Database className="inline w-4 h-4 mr-2 -mt-[2px]" />
|
||||
<b className="mr-2">Data Folder</b>
|
||||
{node?.data_path}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</InputContainer> */}
|
||||
</Card>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader title="Privacy" description="How Spacedrive handles your data" />
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader title="About Spacedrive" description="The file manager from the future." />
|
||||
<span>Version {}</span>
|
||||
<div className="flex flex-col">
|
||||
<span className="mb-1 text-sm font-bold">Created by</span>
|
||||
<span className="max-w-md text-xs text-gray-400">
|
||||
Jamie Pine, Brendan Allan, Oscar Beaumont, Haden Fletcher, Haris Mehrzad Benjamin Akar,
|
||||
and many more.
|
||||
</span>
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
14
packages/interface/src/screens/settings/info/Changelog.tsx
Normal file
14
packages/interface/src/screens/settings/info/Changelog.tsx
Normal file
|
@ -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 (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader title="Changelog" description="See what cool new features we're making" />
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
14
packages/interface/src/screens/settings/info/Support.tsx
Normal file
14
packages/interface/src/screens/settings/info/Support.tsx
Normal file
|
@ -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 (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader title="Support" description="" />
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
|
@ -64,7 +64,7 @@ export default function LibraryGeneralSettings() {
|
|||
/>
|
||||
<div className="flex flex-row pb-3 space-x-5">
|
||||
<div className="flex flex-col flex-grow">
|
||||
<span className="mb-1 font-medium text-gray-700 dark:text-gray-100">Name</span>
|
||||
<span className="mb-1 text-sm font-medium text-gray-700 dark:text-gray-100">Name</span>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
|
@ -72,7 +72,9 @@ export default function LibraryGeneralSettings() {
|
|||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<span className="mb-1 font-medium text-gray-700 dark:text-gray-100">Description</span>
|
||||
<span className="mb-1 text-sm font-medium text-gray-700 dark:text-gray-100">
|
||||
Description
|
||||
</span>
|
||||
<Input
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 | number>(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 (
|
||||
<SettingsContainer>
|
||||
<SettingsHeader title="Tags" description="Manage your tags." />
|
||||
<SettingsHeader
|
||||
title="Tags"
|
||||
description="Manage your tags."
|
||||
rightArea={
|
||||
<div className="flex-row space-x-2">
|
||||
<Dialog
|
||||
open={openCreateModal}
|
||||
onOpenChange={setOpenCreateModal}
|
||||
title="Create New Tag"
|
||||
description="Choose a name and color."
|
||||
ctaAction={() => {
|
||||
createTag({
|
||||
name: newName,
|
||||
color: newColor
|
||||
});
|
||||
}}
|
||||
loading={isLoading}
|
||||
ctaLabel="Create"
|
||||
trigger={
|
||||
<Button variant="primary" size="sm">
|
||||
Create Tag
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="relative mt-3 ">
|
||||
<PopoverPicker
|
||||
className="!absolute left-[9px] -top-[3px]"
|
||||
value={newColor}
|
||||
onChange={setNewColor}
|
||||
/>
|
||||
<Input
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
className="w-full pl-[40px]"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card className="!px-2 dark:bg-gray-800">
|
||||
<div className="flex flex-wrap gap-2 m-1">
|
||||
{tags?.map((tag) => (
|
||||
<div
|
||||
onClick={() => 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' }}
|
||||
>
|
||||
<span className="text-xs text-white drop-shadow-md">{tag.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
{currentTag ? (
|
||||
<form onSubmit={submitTagUpdate}>
|
||||
<div className="flex flex-row mb-10 space-x-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="mb-1 text-sm font-medium text-gray-700 dark:text-gray-100">
|
||||
Color
|
||||
</span>
|
||||
<div className="relative">
|
||||
<Controller
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<PopoverPicker
|
||||
className="!absolute left-[9px] -top-[3px]"
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Input className="w-28 pl-[40px]" {...register('color')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col ">
|
||||
<span className="mb-1 text-sm font-medium text-gray-700 dark:text-gray-100">
|
||||
Name
|
||||
</span>
|
||||
<Input {...register('name')} />
|
||||
</div>
|
||||
<div className="flex flex-grow"></div>
|
||||
<Dialog
|
||||
title="Delete Tag"
|
||||
description="Are you sure you want to delete this tag? This cannot be undone and tagged files will be unlinked."
|
||||
ctaAction={() => {
|
||||
deleteTag({ id: currentTag.id });
|
||||
}}
|
||||
loading={tagDeleteLoading}
|
||||
ctaDanger
|
||||
ctaLabel="Delete"
|
||||
trigger={
|
||||
<Button variant="gray" className="h-[38px] mt-[22px]">
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<InputContainer
|
||||
mini
|
||||
title="Show in Spaces"
|
||||
description="Show this tag on the spaces screen."
|
||||
>
|
||||
<Toggle value />
|
||||
</InputContainer>
|
||||
</form>
|
||||
) : (
|
||||
<div className="text-sm font-medium text-gray-400">No Tag Selected</div>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ export default function LibrarySettings() {
|
|||
}
|
||||
});
|
||||
|
||||
const { data: libraries } = useBridgeQuery('NodeGetLibraries');
|
||||
const { data: libraries } = useBridgeQuery('GetLibraries');
|
||||
|
||||
function createNewLib() {
|
||||
if (newLibName) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue