mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-18 07:19:10 +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": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Spacedrive",
|
"title": "Spacedrive",
|
||||||
"width": 1200,
|
"width": 1400,
|
||||||
"height": 725,
|
"height": 725,
|
||||||
"minWidth": 700,
|
"minWidth": 700,
|
||||||
"minHeight": 500,
|
"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.
|
// 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";
|
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 { File } from "./File";
|
||||||
import type { JobReport } from "./JobReport";
|
import type { JobReport } from "./JobReport";
|
||||||
import type { LocationResource } from "./LocationResource";
|
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 { LocationResource } from "./LocationResource";
|
||||||
import type { NodeState } from "./NodeState";
|
import type { NodeState } from "./NodeState";
|
||||||
import type { Statistics } from "./Statistics";
|
import type { Statistics } from "./Statistics";
|
||||||
|
import type { Tag } from "./Tag";
|
||||||
|
import type { TagWithFiles } from "./TagWithFiles";
|
||||||
import type { Volume } from "./Volume";
|
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.
|
// 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.
|
// 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/NodeState';
|
||||||
export * from './bindings/Platform';
|
export * from './bindings/Platform';
|
||||||
export * from './bindings/Statistics';
|
export * from './bindings/Statistics';
|
||||||
|
export * from './bindings/Tag';
|
||||||
|
export * from './bindings/TagOnFile';
|
||||||
|
export * from './bindings/TagWithFiles';
|
||||||
export * from './bindings/Volume';
|
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[]
|
tags TagOnFile[]
|
||||||
labels LabelOnFile[]
|
labels LabelOnFile[]
|
||||||
albums FileInAlbum[]
|
albums FileInAlbum[]
|
||||||
|
spaces FileInSpace[]
|
||||||
paths FilePath[]
|
paths FilePath[]
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
media_data MediaData?
|
media_data MediaData?
|
||||||
|
@ -227,6 +228,7 @@ model Tag {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
pub_id String @unique
|
pub_id String @unique
|
||||||
name String?
|
name String?
|
||||||
|
color String?
|
||||||
total_files Int? @default(0)
|
total_files Int? @default(0)
|
||||||
redundancy_goal Int? @default(1)
|
redundancy_goal Int? @default(1)
|
||||||
date_created DateTime @default(now())
|
date_created DateTime @default(now())
|
||||||
|
@ -246,7 +248,7 @@ model TagOnFile {
|
||||||
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||||
|
|
||||||
@@id([tag_id, file_id])
|
@@id([tag_id, file_id])
|
||||||
@@map("tags_on_files")
|
@@map("tags_on_file")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Label {
|
model Label {
|
||||||
|
@ -270,7 +272,32 @@ model LabelOnFile {
|
||||||
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||||
|
|
||||||
@@id([label_id, file_id])
|
@@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 {
|
model Job {
|
||||||
|
|
|
@ -2,8 +2,9 @@ use crate::{
|
||||||
encode::THUMBNAIL_CACHE_DIR_NAME,
|
encode::THUMBNAIL_CACHE_DIR_NAME,
|
||||||
file::{DirectoryWithContents, FileError, FilePath},
|
file::{DirectoryWithContents, FileError, FilePath},
|
||||||
library::LibraryContext,
|
library::LibraryContext,
|
||||||
prisma::file_path,
|
prisma::{file_path, tag, tag_on_file},
|
||||||
sys::get_location,
|
sys::get_location,
|
||||||
|
tag::{Tag, TagError, TagOnFile, TagWithFiles},
|
||||||
};
|
};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
@ -60,3 +61,29 @@ pub async fn open_dir(
|
||||||
contents: file_paths,
|
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 {
|
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
||||||
library_id: ctx.id.to_string(),
|
library_id: ctx.id.to_string(),
|
||||||
query: LibraryQuery::LibGetExplorerDir {
|
query: LibraryQuery::GetExplorerDir {
|
||||||
limit: 0,
|
limit: 0,
|
||||||
path: "".to_string(),
|
path: "".to_string(),
|
||||||
location_id: 0,
|
location_id: 0,
|
||||||
|
@ -185,7 +185,7 @@ pub async fn favorite(
|
||||||
|
|
||||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
||||||
library_id: ctx.id.to_string(),
|
library_id: ctx.id.to_string(),
|
||||||
query: LibraryQuery::LibGetExplorerDir {
|
query: LibraryQuery::GetExplorerDir {
|
||||||
limit: 0,
|
limit: 0,
|
||||||
path: "".to_string(),
|
path: "".to_string(),
|
||||||
location_id: 0,
|
location_id: 0,
|
||||||
|
|
|
@ -3,6 +3,7 @@ use super::{
|
||||||
Job, JobManager,
|
Job, JobManager,
|
||||||
};
|
};
|
||||||
use crate::{library::LibraryContext, ClientQuery, CoreEvent, LibraryQuery};
|
use crate::{library::LibraryContext, ClientQuery, CoreEvent, LibraryQuery};
|
||||||
|
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::{
|
sync::{
|
||||||
|
@ -172,7 +173,10 @@ impl Worker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.emit(CoreEvent::InvalidateQueryDebounced(
|
ctx.emit(CoreEvent::InvalidateQueryDebounced(
|
||||||
ClientQuery::JobGetRunning,
|
ClientQuery::LibraryQuery {
|
||||||
|
library_id: ctx.id.to_string(),
|
||||||
|
query: LibraryQuery::GetRunningJobs,
|
||||||
|
},
|
||||||
))
|
))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -180,12 +184,15 @@ impl Worker {
|
||||||
worker.job_report.status = JobStatus::Completed;
|
worker.job_report.status = JobStatus::Completed;
|
||||||
worker.job_report.update(&ctx).await.unwrap_or(());
|
worker.job_report.update(&ctx).await.unwrap_or(());
|
||||||
|
|
||||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::JobGetRunning))
|
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
||||||
.await;
|
library_id: ctx.id.to_string(),
|
||||||
|
query: LibraryQuery::GetRunningJobs,
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
|
||||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
||||||
library_id: ctx.id.to_string(),
|
library_id: ctx.id.to_string(),
|
||||||
query: LibraryQuery::JobGetHistory,
|
query: LibraryQuery::GetJobHistory,
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
break;
|
break;
|
||||||
|
@ -196,9 +203,10 @@ impl Worker {
|
||||||
|
|
||||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
||||||
library_id: ctx.id.to_string(),
|
library_id: ctx.id.to_string(),
|
||||||
query: LibraryQuery::JobGetHistory,
|
query: LibraryQuery::GetJobHistory,
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
176
core/src/lib.rs
176
core/src/lib.rs
|
@ -8,6 +8,8 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
use tag::{Tag, TagWithFiles};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::sync::{
|
use tokio::sync::{
|
||||||
mpsc::{self, unbounded_channel, UnboundedReceiver, UnboundedSender},
|
mpsc::{self, unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||||
|
@ -24,6 +26,7 @@ mod library;
|
||||||
mod node;
|
mod node;
|
||||||
mod prisma;
|
mod prisma;
|
||||||
mod sys;
|
mod sys;
|
||||||
|
mod tag;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
// a wrapper around external input with a returning sender channel for core to respond
|
// a wrapper around external input with a returning sender channel for core to respond
|
||||||
|
@ -235,15 +238,18 @@ impl Node {
|
||||||
CoreResponse::Success(())
|
CoreResponse::Success(())
|
||||||
}
|
}
|
||||||
// CRUD for tags
|
// CRUD for tags
|
||||||
LibraryCommand::TagCreate { name: _, color: _ } => todo!(),
|
LibraryCommand::TagCreate { name, color } => {
|
||||||
LibraryCommand::TagAssign {
|
tag::create_tag(ctx, name, color).await?
|
||||||
file_id: _,
|
}
|
||||||
tag_id: _,
|
LibraryCommand::TagAssign { file_id, tag_id } => {
|
||||||
} => todo!(),
|
tag::tag_assign(ctx, file_id, tag_id).await?
|
||||||
LibraryCommand::TagUpdate { name: _, color: _ } => todo!(),
|
}
|
||||||
LibraryCommand::TagDelete { id: _ } => todo!(),
|
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
|
// CRUD for libraries
|
||||||
LibraryCommand::SysVolumeUnmount { id: _ } => todo!(),
|
LibraryCommand::VolUnmount { id: _ } => todo!(),
|
||||||
LibraryCommand::GenerateThumbsForLocation { id, path } => {
|
LibraryCommand::GenerateThumbsForLocation { id, path } => {
|
||||||
ctx.spawn_job(Box::new(ThumbnailJob {
|
ctx.spawn_job(Box::new(ThumbnailJob {
|
||||||
location_id: id,
|
location_id: id,
|
||||||
|
@ -269,18 +275,15 @@ impl Node {
|
||||||
// query sources of data
|
// query sources of data
|
||||||
async fn exec_query(&self, query: ClientQuery) -> Result<CoreResponse, CoreError> {
|
async fn exec_query(&self, query: ClientQuery) -> Result<CoreResponse, CoreError> {
|
||||||
Ok(match query {
|
Ok(match query {
|
||||||
ClientQuery::NodeGetLibraries => CoreResponse::NodeGetLibraries(
|
ClientQuery::GetLibraries => {
|
||||||
self.library_manager.get_all_libraries_config().await,
|
CoreResponse::GetLibraries(self.library_manager.get_all_libraries_config().await)
|
||||||
),
|
}
|
||||||
ClientQuery::NodeGetState => CoreResponse::NodeGetState(NodeState {
|
ClientQuery::GetNode => CoreResponse::GetNode(NodeState {
|
||||||
config: self.config.get().await,
|
config: self.config.get().await,
|
||||||
data_path: self.config.data_directory().to_str().unwrap().to_string(),
|
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::GetNodes => todo!(),
|
||||||
|
ClientQuery::GetVolumes => CoreResponse::GetVolumes(sys::Volume::get_volumes()?),
|
||||||
ClientQuery::LibraryQuery { library_id, query } => {
|
ClientQuery::LibraryQuery { library_id, query } => {
|
||||||
let ctx = match self.library_manager.get_ctx(library_id.clone()).await {
|
let ctx = match self.library_manager.get_ctx(library_id.clone()).await {
|
||||||
Some(ctx) => ctx,
|
Some(ctx) => ctx,
|
||||||
|
@ -290,28 +293,34 @@ impl Node {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match query {
|
match query {
|
||||||
LibraryQuery::SysGetLocations => {
|
LibraryQuery::GetLocations => {
|
||||||
CoreResponse::SysGetLocations(sys::get_locations(&ctx).await?)
|
CoreResponse::GetLocations(sys::get_locations(&ctx).await?)
|
||||||
|
}
|
||||||
|
LibraryQuery::GetRunningJobs => {
|
||||||
|
CoreResponse::GetRunningJobs(self.jobs.get_running().await)
|
||||||
}
|
}
|
||||||
// get location from library
|
// get location from library
|
||||||
LibraryQuery::SysGetLocation { id } => {
|
LibraryQuery::GetLocation { id } => {
|
||||||
CoreResponse::SysGetLocation(sys::get_location(&ctx, id).await?)
|
CoreResponse::GetLocation(sys::get_location(&ctx, id).await?)
|
||||||
}
|
}
|
||||||
// return contents of a directory for the explorer
|
// return contents of a directory for the explorer
|
||||||
LibraryQuery::LibGetExplorerDir {
|
LibraryQuery::GetExplorerDir {
|
||||||
path,
|
path,
|
||||||
location_id,
|
location_id,
|
||||||
limit: _,
|
limit: _,
|
||||||
} => CoreResponse::LibGetExplorerDir(
|
} => CoreResponse::GetExplorerDir(
|
||||||
file::explorer::open_dir(&ctx, &location_id, &path).await?,
|
file::explorer::open_dir(&ctx, &location_id, &path).await?,
|
||||||
),
|
),
|
||||||
LibraryQuery::LibGetTags => todo!(),
|
LibraryQuery::GetJobHistory => {
|
||||||
LibraryQuery::JobGetHistory => {
|
CoreResponse::GetJobHistory(JobManager::get_history(&ctx).await?)
|
||||||
CoreResponse::JobGetHistory(JobManager::get_history(&ctx).await?)
|
|
||||||
}
|
}
|
||||||
LibraryQuery::GetLibraryStatistics => CoreResponse::GetLibraryStatistics(
|
LibraryQuery::GetLibraryStatistics => CoreResponse::GetLibraryStatistics(
|
||||||
library::Statistics::calculate(&ctx).await?,
|
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)]
|
#[ts(export)]
|
||||||
pub enum LibraryCommand {
|
pub enum LibraryCommand {
|
||||||
// Files
|
// Files
|
||||||
FileReadMetaData { id: i32 },
|
FileReadMetaData {
|
||||||
FileSetNote { id: i32, note: Option<String> },
|
id: i32,
|
||||||
FileSetFavorite { id: i32, favorite: bool },
|
},
|
||||||
|
FileSetNote {
|
||||||
|
id: i32,
|
||||||
|
note: Option<String>,
|
||||||
|
},
|
||||||
|
FileSetFavorite {
|
||||||
|
id: i32,
|
||||||
|
favorite: bool,
|
||||||
|
},
|
||||||
// FileEncrypt { id: i32, algorithm: EncryptionAlgorithm },
|
// FileEncrypt { id: i32, algorithm: EncryptionAlgorithm },
|
||||||
FileDelete { id: i32 },
|
FileDelete {
|
||||||
|
id: i32,
|
||||||
|
},
|
||||||
// Tags
|
// Tags
|
||||||
TagCreate { name: String, color: String },
|
TagCreate {
|
||||||
TagUpdate { name: String, color: String },
|
name: String,
|
||||||
TagAssign { file_id: i32, tag_id: i32 },
|
color: String,
|
||||||
TagDelete { id: i32 },
|
},
|
||||||
|
TagUpdate {
|
||||||
|
id: i32,
|
||||||
|
name: Option<String>,
|
||||||
|
color: Option<String>,
|
||||||
|
},
|
||||||
|
TagAssign {
|
||||||
|
file_id: i32,
|
||||||
|
tag_id: i32,
|
||||||
|
},
|
||||||
|
TagDelete {
|
||||||
|
id: i32,
|
||||||
|
},
|
||||||
// Locations
|
// Locations
|
||||||
LocCreate { path: String },
|
LocCreate {
|
||||||
LocUpdate { id: i32, name: Option<String> },
|
path: String,
|
||||||
LocDelete { id: i32 },
|
},
|
||||||
LocFullRescan { id: i32 },
|
LocUpdate {
|
||||||
LocQuickRescan { id: i32 },
|
id: i32,
|
||||||
|
name: Option<String>,
|
||||||
|
},
|
||||||
|
LocDelete {
|
||||||
|
id: i32,
|
||||||
|
},
|
||||||
|
LocFullRescan {
|
||||||
|
id: i32,
|
||||||
|
},
|
||||||
|
LocQuickRescan {
|
||||||
|
id: i32,
|
||||||
|
},
|
||||||
// System
|
// System
|
||||||
SysVolumeUnmount { id: i32 },
|
VolUnmount {
|
||||||
GenerateThumbsForLocation { id: i32, path: String },
|
id: i32,
|
||||||
|
},
|
||||||
|
GenerateThumbsForLocation {
|
||||||
|
id: i32,
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
// PurgeDatabase,
|
// PurgeDatabase,
|
||||||
IdentifyUniqueFiles { id: i32, path: String },
|
IdentifyUniqueFiles {
|
||||||
|
id: i32,
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// is a query destined for the core
|
/// is a query destined for the core
|
||||||
|
@ -375,10 +425,9 @@ pub enum LibraryCommand {
|
||||||
#[serde(tag = "key", content = "params")]
|
#[serde(tag = "key", content = "params")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub enum ClientQuery {
|
pub enum ClientQuery {
|
||||||
NodeGetLibraries,
|
GetLibraries,
|
||||||
NodeGetState,
|
GetNode,
|
||||||
SysGetVolumes,
|
GetVolumes,
|
||||||
JobGetRunning,
|
|
||||||
GetNodes,
|
GetNodes,
|
||||||
LibraryQuery {
|
LibraryQuery {
|
||||||
library_id: String,
|
library_id: String,
|
||||||
|
@ -391,18 +440,22 @@ pub enum ClientQuery {
|
||||||
#[serde(tag = "key", content = "params")]
|
#[serde(tag = "key", content = "params")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub enum LibraryQuery {
|
pub enum LibraryQuery {
|
||||||
LibGetTags,
|
GetJobHistory,
|
||||||
JobGetHistory,
|
GetLocations,
|
||||||
SysGetLocations,
|
GetLocation {
|
||||||
SysGetLocation {
|
|
||||||
id: i32,
|
id: i32,
|
||||||
},
|
},
|
||||||
LibGetExplorerDir {
|
GetRunningJobs,
|
||||||
|
GetExplorerDir {
|
||||||
location_id: i32,
|
location_id: i32,
|
||||||
path: String,
|
path: String,
|
||||||
limit: i32,
|
limit: i32,
|
||||||
},
|
},
|
||||||
GetLibraryStatistics,
|
GetLibraryStatistics,
|
||||||
|
GetTags,
|
||||||
|
GetFilesTagged {
|
||||||
|
tag_id: i32,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// represents an event this library can emit
|
// represents an event this library can emit
|
||||||
|
@ -433,15 +486,19 @@ pub struct NodeState {
|
||||||
pub enum CoreResponse {
|
pub enum CoreResponse {
|
||||||
Success(()),
|
Success(()),
|
||||||
Error(String),
|
Error(String),
|
||||||
NodeGetLibraries(Vec<LibraryConfigWrapped>),
|
GetLibraries(Vec<LibraryConfigWrapped>),
|
||||||
SysGetVolumes(Vec<sys::Volume>),
|
GetVolumes(Vec<sys::Volume>),
|
||||||
SysGetLocation(sys::LocationResource),
|
TagCreateResponse(tag::Tag),
|
||||||
SysGetLocations(Vec<sys::LocationResource>),
|
GetTag(Option<tag::Tag>),
|
||||||
LibGetExplorerDir(file::DirectoryWithContents),
|
GetTags(Vec<tag::Tag>),
|
||||||
NodeGetState(NodeState),
|
GetLocation(sys::LocationResource),
|
||||||
|
GetLocations(Vec<sys::LocationResource>),
|
||||||
|
GetExplorerDir(file::DirectoryWithContents),
|
||||||
|
GetNode(NodeState),
|
||||||
LocCreate(sys::LocationResource),
|
LocCreate(sys::LocationResource),
|
||||||
JobGetRunning(Vec<JobReport>),
|
OpenTag(Vec<tag::TagWithFiles>),
|
||||||
JobGetHistory(Vec<JobReport>),
|
GetRunningJobs(Vec<JobReport>),
|
||||||
|
GetJobHistory(Vec<JobReport>),
|
||||||
GetLibraryStatistics(library::Statistics),
|
GetLibraryStatistics(library::Statistics),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -462,6 +519,7 @@ pub enum CoreError {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||||
|
#[serde(tag = "key", content = "data")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub enum CoreResource {
|
pub enum CoreResource {
|
||||||
Client,
|
Client,
|
||||||
|
@ -469,5 +527,5 @@ pub enum CoreResource {
|
||||||
Location(sys::LocationResource),
|
Location(sys::LocationResource),
|
||||||
File(file::File),
|
File(file::File),
|
||||||
Job(JobReport),
|
Job(JobReport),
|
||||||
Tag,
|
Tag(tag::Tag),
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,7 +138,7 @@ impl LibraryManager {
|
||||||
self.libraries.write().await.push(library);
|
self.libraries.write().await.push(library);
|
||||||
|
|
||||||
self.node_context
|
self.node_context
|
||||||
.emit(CoreEvent::InvalidateQuery(ClientQuery::NodeGetLibraries))
|
.emit(CoreEvent::InvalidateQuery(ClientQuery::GetLibraries))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -184,7 +184,7 @@ impl LibraryManager {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.node_context
|
self.node_context
|
||||||
.emit(CoreEvent::InvalidateQuery(ClientQuery::NodeGetLibraries))
|
.emit(CoreEvent::InvalidateQuery(ClientQuery::GetLibraries))
|
||||||
.await;
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -209,7 +209,7 @@ impl LibraryManager {
|
||||||
libraries.retain(|l| l.id != id);
|
libraries.retain(|l| l.id != id);
|
||||||
|
|
||||||
self.node_context
|
self.node_context
|
||||||
.emit(CoreEvent::InvalidateQuery(ClientQuery::NodeGetLibraries))
|
.emit(CoreEvent::InvalidateQuery(ClientQuery::GetLibraries))
|
||||||
.await;
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,7 +212,7 @@ pub async fn create_location(
|
||||||
Err(e) => Err(LocationError::DotfileWriteFailure(e, path.to_string()))?,
|
Err(e) => Err(LocationError::DotfileWriteFailure(e, path.to_string()))?,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::SysGetLocations))
|
// ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::GetLocations))
|
||||||
// .await;
|
// .await;
|
||||||
|
|
||||||
location
|
location
|
||||||
|
@ -239,7 +239,7 @@ pub async fn delete_location(ctx: &LibraryContext, location_id: i32) -> Result<(
|
||||||
|
|
||||||
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::LibraryQuery {
|
||||||
library_id: ctx.id.to_string(),
|
library_id: ctx.id.to_string(),
|
||||||
query: LibraryQuery::SysGetLocations,
|
query: LibraryQuery::GetLocations,
|
||||||
}))
|
}))
|
||||||
.await;
|
.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
|
// is memorized and can be used safely in any component
|
||||||
export const useCurrentLibrary = () => {
|
export const useCurrentLibrary = () => {
|
||||||
const { currentLibraryUuid, switchLibrary } = useLibraryStore();
|
const { currentLibraryUuid, switchLibrary } = useLibraryStore();
|
||||||
const { data: libraries } = useBridgeQuery('NodeGetLibraries', undefined, {});
|
const { data: libraries } = useBridgeQuery('GetLibraries', undefined, {});
|
||||||
|
|
||||||
// memorize library to avoid re-running find function
|
// memorize library to avoid re-running find function
|
||||||
const currentLibrary = useMemo(() => {
|
const currentLibrary = useMemo(() => {
|
||||||
|
|
|
@ -38,10 +38,12 @@
|
||||||
"phosphor-react": "^1.4.1",
|
"phosphor-react": "^1.4.1",
|
||||||
"pretty-bytes": "^6.0.0",
|
"pretty-bytes": "^6.0.0",
|
||||||
"react": "^18.1.0",
|
"react": "^18.1.0",
|
||||||
|
"react-colorful": "^5.5.1",
|
||||||
"react-countup": "^6.2.0",
|
"react-countup": "^6.2.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.1.0",
|
||||||
"react-dropzone": "^14.2.1",
|
"react-dropzone": "^14.2.1",
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
|
"react-hook-form": "^7.31.3",
|
||||||
"react-hotkeys-hook": "^3.4.6",
|
"react-hotkeys-hook": "^3.4.6",
|
||||||
"react-json-view": "^1.21.3",
|
"react-json-view": "^1.21.3",
|
||||||
"react-loading-icons": "^1.1.0",
|
"react-loading-icons": "^1.1.0",
|
||||||
|
|
|
@ -16,7 +16,7 @@ const queryClient = new QueryClient();
|
||||||
function RouterContainer(props: { props: AppProps }) {
|
function RouterContainer(props: { props: AppProps }) {
|
||||||
useCoreEvents();
|
useCoreEvents();
|
||||||
const [appProps, setAppProps] = useState(props.props);
|
const [appProps, setAppProps] = useState(props.props);
|
||||||
const { data: client } = useBridgeQuery('NodeGetState');
|
const { data: client } = useBridgeQuery('GetNode');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAppProps({
|
setAppProps({
|
||||||
|
|
|
@ -17,6 +17,10 @@ import AppearanceSettings from './screens/settings/client/AppearanceSettings';
|
||||||
import ExtensionSettings from './screens/settings/client/ExtensionsSettings';
|
import ExtensionSettings from './screens/settings/client/ExtensionsSettings';
|
||||||
import GeneralSettings from './screens/settings/client/GeneralSettings';
|
import GeneralSettings from './screens/settings/client/GeneralSettings';
|
||||||
import KeybindSettings from './screens/settings/client/KeybindSettings';
|
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 ContactsSettings from './screens/settings/library/ContactsSettings';
|
||||||
import KeysSettings from './screens/settings/library/KeysSetting';
|
import KeysSettings from './screens/settings/library/KeysSetting';
|
||||||
import LibraryGeneralSettings from './screens/settings/library/LibraryGeneralSettings';
|
import LibraryGeneralSettings from './screens/settings/library/LibraryGeneralSettings';
|
||||||
|
@ -34,7 +38,7 @@ export function AppRouter() {
|
||||||
let location = useLocation();
|
let location = useLocation();
|
||||||
let state = location.state as { backgroundLocation?: Location };
|
let state = location.state as { backgroundLocation?: Location };
|
||||||
const libraryState = useLibraryStore();
|
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
|
// TODO: This can be removed once we add a setup flow to the app
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -79,6 +83,10 @@ export function AppRouter() {
|
||||||
<Route path="tags" element={<TagsSettings />} />
|
<Route path="tags" element={<TagsSettings />} />
|
||||||
<Route path="nodes" element={<NodesSettings />} />
|
<Route path="nodes" element={<NodesSettings />} />
|
||||||
<Route path="keys" element={<KeysSettings />} />
|
<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>
|
||||||
<Route path="explorer/:id" element={<ExplorerScreen />} />
|
<Route path="explorer/:id" element={<ExplorerScreen />} />
|
||||||
<Route path="tag/:id" element={<TagScreen />} />
|
<Route path="tag/:id" element={<TagScreen />} />
|
||||||
|
|
|
@ -21,7 +21,7 @@ interface IColumn {
|
||||||
|
|
||||||
const PADDING_SIZE = 130;
|
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) {
|
function ensureIsColumns<T extends IColumn[]>(data: T) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -48,20 +48,19 @@ const GridItemContainer = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FileList: React.FC<{ location_id: number; path: string; limit: number }> = (props) => {
|
export const FileList: React.FC<{ location_id: number; path: string; limit: number }> = (props) => {
|
||||||
|
const path = props.path;
|
||||||
const size = useWindowSize();
|
const size = useWindowSize();
|
||||||
const tableContainer = useRef<null | HTMLDivElement>(null);
|
const tableContainer = useRef<null | HTMLDivElement>(null);
|
||||||
const VList = useRef<null | VirtuosoHandle>(null);
|
const VList = useRef<null | VirtuosoHandle>(null);
|
||||||
|
|
||||||
const { data: client } = useBridgeQuery('NodeGetState', undefined, {
|
const { data: client } = useBridgeQuery('GetNode', undefined, {
|
||||||
refetchOnWindowFocus: false
|
refetchOnWindowFocus: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const path = props.path;
|
|
||||||
|
|
||||||
const { selectedRowIndex, setSelectedRowIndex, setLocationId, layoutMode } = useExplorerStore();
|
const { selectedRowIndex, setSelectedRowIndex, setLocationId, layoutMode } = useExplorerStore();
|
||||||
const [goingUp, setGoingUp] = useState(false);
|
const [goingUp, setGoingUp] = useState(false);
|
||||||
|
|
||||||
const { data: currentDir } = useLibraryQuery('LibGetExplorerDir', {
|
const { data: currentDir } = useLibraryQuery('GetExplorerDir', {
|
||||||
location_id: props.location_id,
|
location_id: props.location_id,
|
||||||
path,
|
path,
|
||||||
limit: props.limit
|
limit: props.limit
|
||||||
|
@ -124,11 +123,7 @@ export const FileList: React.FC<{ location_id: number; path: string; limit: numb
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={tableContainer} style={{ marginTop: -44 }} className="w-full pl-2 cursor-default ">
|
||||||
ref={tableContainer}
|
|
||||||
style={{ marginTop: -44 }}
|
|
||||||
className="w-full pl-2 bg-white cursor-default dark:bg-gray-600"
|
|
||||||
>
|
|
||||||
<LocationContext.Provider
|
<LocationContext.Provider
|
||||||
value={{ location_id: props.location_id, data_path: client?.data_path as string }}
|
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 }) => (
|
{({ isActive }) => (
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
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
|
'!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 appProps = useContext(AppPropsContext);
|
||||||
|
|
||||||
const { data: locationsResponse, isError: isLocationsError } = useLibraryQuery('SysGetLocations');
|
const { data: locationsResponse, isError: isLocationsError } = useLibraryQuery('GetLocations');
|
||||||
|
|
||||||
let locations = Array.isArray(locationsResponse) ? locationsResponse : [];
|
let locations = Array.isArray(locationsResponse) ? locationsResponse : [];
|
||||||
|
|
||||||
|
@ -96,13 +96,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||||
|
|
||||||
const { mutate: createLocation } = useLibraryCommand('LocCreate');
|
const { mutate: createLocation } = useLibraryCommand('LocCreate');
|
||||||
|
|
||||||
const tags = [
|
const { data: tags } = useLibraryQuery('GetTags');
|
||||||
{ 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' }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -192,15 +186,6 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||||
<Icon component={PhotographIcon} />
|
<Icon component={PhotographIcon} />
|
||||||
Photos
|
Photos
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
{isExperimental ? (
|
|
||||||
<SidebarLink to="debug">
|
|
||||||
<Icon component={Code} />
|
|
||||||
Debug
|
|
||||||
</SidebarLink>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Heading>Locations</Heading>
|
<Heading>Locations</Heading>
|
||||||
|
@ -256,11 +241,11 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||||
<div>
|
<div>
|
||||||
<Heading>Tags</Heading>
|
<Heading>Tags</Heading>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
{tags.map((tag, index) => (
|
{tags?.slice(0, 6).map((tag, index) => (
|
||||||
<SidebarLink key={index} to={`tag/${tag.id}`} className="">
|
<SidebarLink key={index} to={`tag/${tag.id}`} className="">
|
||||||
<div
|
<div
|
||||||
className="w-[12px] h-[12px] rounded-full"
|
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>
|
<span className="ml-2 text-sm">{tag.name}</span>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
|
@ -51,7 +51,7 @@ const MiddleTruncatedText = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RunningJobsWidget() {
|
export default function RunningJobsWidget() {
|
||||||
const { data: jobs } = useBridgeQuery('JobGetRunning');
|
const { data: jobs } = useBridgeQuery('GetRunningJobs');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
|
|
|
@ -31,19 +31,27 @@ export interface TopBarButtonProps
|
||||||
}
|
}
|
||||||
interface SearchBarProps extends DefaultProps {}
|
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 (
|
return (
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
className={clsx(
|
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',
|
'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 rounded-l-none': group && !left && !right,
|
||||||
'rounded-r-none': props.group && props.left,
|
'rounded-r-none': group && left,
|
||||||
'rounded-l-none': props.group && props.right,
|
'rounded-l-none': group && right,
|
||||||
'dark:bg-gray-550': props.active
|
'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" />
|
<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)}
|
className={clsx('flex flex-col w-full', !props.mini && 'pb-6', props.className)}
|
||||||
{...props}
|
{...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.description && <p className="mb-2 text-sm text-gray-400 ">{props.description}</p>}
|
||||||
{!props.mini && props.children}
|
{!props.mini && props.children}
|
||||||
</div>
|
</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) => {
|
export const DebugScreen: React.FC<{}> = (props) => {
|
||||||
const appPropsContext = useContext(AppPropsContext);
|
const appPropsContext = useContext(AppPropsContext);
|
||||||
const { data: nodeState } = useBridgeQuery('NodeGetState');
|
const { data: nodeState } = useBridgeQuery('GetNode');
|
||||||
const { data: libraryState } = useBridgeQuery('NodeGetLibraries');
|
const { data: libraryState } = useBridgeQuery('GetLibraries');
|
||||||
const { data: jobs } = useBridgeQuery('JobGetRunning');
|
const { data: jobs } = useBridgeQuery('GetRunningJobs');
|
||||||
const { data: jobHistory } = useLibraryQuery('JobGetHistory');
|
const { data: jobHistory } = useLibraryQuery('GetJobHistory');
|
||||||
// const { mutate: purgeDB } = useBridgeCommand('PurgeDatabase', {
|
// const { mutate: purgeDB } = useBridgeCommand('PurgeDatabase', {
|
||||||
// onMutate: () => {
|
// onMutate: () => {
|
||||||
// alert('Database purged');
|
// alert('Database purged');
|
||||||
|
|
|
@ -19,17 +19,17 @@ export const ExplorerScreen: React.FC<{}> = () => {
|
||||||
const { selectedRowIndex } = useExplorerStore();
|
const { selectedRowIndex } = useExplorerStore();
|
||||||
|
|
||||||
// Current Location
|
// Current Location
|
||||||
const { data: currentLocation } = useLibraryQuery('SysGetLocation', { id: location_id });
|
const { data: currentLocation } = useLibraryQuery('GetLocation', { id: location_id });
|
||||||
|
|
||||||
// Current Directory
|
// Current Directory
|
||||||
const { data: currentDir } = useLibraryQuery(
|
const { data: currentDir } = useLibraryQuery(
|
||||||
'LibGetExplorerDir',
|
'GetExplorerDir',
|
||||||
{ location_id: location_id!, path, limit },
|
{ location_id: location_id!, path, limit },
|
||||||
{ enabled: !!location_id }
|
{ enabled: !!location_id }
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col w-full bg-gray-600">
|
<div className="relative flex flex-col w-full bg-gray-650">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<div className="relative flex flex-row w-full max-h-full">
|
<div className="relative flex flex-row w-full max-h-full">
|
||||||
<FileList location_id={location_id} path={path} limit={limit} />
|
<FileList location_id={location_id} path={path} limit={limit} />
|
||||||
|
|
|
@ -102,7 +102,7 @@ const StatItem: React.FC<StatItemProps> = (props) => {
|
||||||
export const OverviewScreen = () => {
|
export const OverviewScreen = () => {
|
||||||
const { data: libraryStatistics, isLoading: isStatisticsLoading } =
|
const { data: libraryStatistics, isLoading: isStatisticsLoading } =
|
||||||
useLibraryQuery('GetLibraryStatistics');
|
useLibraryQuery('GetLibraryStatistics');
|
||||||
const { data: nodeState } = useBridgeQuery('NodeGetState');
|
const { data: nodeState } = useBridgeQuery('GetNode');
|
||||||
|
|
||||||
const { overviewStats, setOverviewStats } = useOverviewState();
|
const { overviewStats, setOverviewStats } = useOverviewState();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
AnnotationIcon,
|
||||||
CogIcon,
|
CogIcon,
|
||||||
CollectionIcon,
|
CollectionIcon,
|
||||||
DatabaseIcon,
|
DatabaseIcon,
|
||||||
|
@ -8,12 +9,17 @@ import {
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
LibraryIcon,
|
LibraryIcon,
|
||||||
LightBulbIcon,
|
LightBulbIcon,
|
||||||
|
LockClosedIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
|
SparklesIcon,
|
||||||
TagIcon,
|
TagIcon,
|
||||||
TerminalIcon
|
TerminalIcon
|
||||||
} from '@heroicons/react/outline';
|
} from '@heroicons/react/outline';
|
||||||
import {
|
import {
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Cloud,
|
Cloud,
|
||||||
|
FlyingSaucer,
|
||||||
|
HandWaving,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
Hash,
|
Hash,
|
||||||
Info,
|
Info,
|
||||||
|
@ -21,6 +27,7 @@ import {
|
||||||
PaintBrush,
|
PaintBrush,
|
||||||
PuzzlePiece,
|
PuzzlePiece,
|
||||||
ShareNetwork,
|
ShareNetwork,
|
||||||
|
Shield,
|
||||||
UsersFour
|
UsersFour
|
||||||
} from 'phosphor-react';
|
} from 'phosphor-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
@ -44,6 +51,10 @@ export const SettingsScreen: React.FC = () => {
|
||||||
<SettingsIcon component={CollectionIcon} />
|
<SettingsIcon component={CollectionIcon} />
|
||||||
Libraries
|
Libraries
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
<SidebarLink to="/settings/privacy">
|
||||||
|
<SettingsIcon component={ShieldCheckIcon} />
|
||||||
|
Privacy
|
||||||
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/appearance">
|
<SidebarLink to="/settings/appearance">
|
||||||
<SettingsIcon component={PaintBrush} />
|
<SettingsIcon component={PaintBrush} />
|
||||||
Appearance
|
Appearance
|
||||||
|
@ -86,7 +97,7 @@ export const SettingsScreen: React.FC = () => {
|
||||||
<SettingsIcon component={ShareNetwork} />
|
<SettingsIcon component={ShareNetwork} />
|
||||||
Sync
|
Sync
|
||||||
</SidebarLink> */}
|
</SidebarLink> */}
|
||||||
<SettingsHeading>Advanced</SettingsHeading>
|
{/* <SettingsHeading>Advanced</SettingsHeading>
|
||||||
<SidebarLink to="/settings/p2p">
|
<SidebarLink to="/settings/p2p">
|
||||||
<SettingsIcon component={ShareNetwork} />
|
<SettingsIcon component={ShareNetwork} />
|
||||||
Networking
|
Networking
|
||||||
|
@ -94,15 +105,15 @@ export const SettingsScreen: React.FC = () => {
|
||||||
<SidebarLink to="/settings/experimental">
|
<SidebarLink to="/settings/experimental">
|
||||||
<SettingsIcon component={TerminalIcon} />
|
<SettingsIcon component={TerminalIcon} />
|
||||||
Developer
|
Developer
|
||||||
</SidebarLink>
|
</SidebarLink> */}
|
||||||
|
|
||||||
<SettingsHeading>Resources</SettingsHeading>
|
<SettingsHeading>Resources</SettingsHeading>
|
||||||
<SidebarLink to="/settings/about">
|
<SidebarLink to="/settings/about">
|
||||||
<SettingsIcon component={BookOpen} />
|
<SettingsIcon component={FlyingSaucer} />
|
||||||
About
|
About
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/changelog">
|
<SidebarLink to="/settings/changelog">
|
||||||
<SettingsIcon component={LightBulbIcon} />
|
<SettingsIcon component={AnnotationIcon} />
|
||||||
Changelog
|
Changelog
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/support">
|
<SidebarLink to="/settings/support">
|
||||||
|
|
|
@ -58,7 +58,7 @@ function ExtensionItem(props: { extension: ExtensionItemData }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExtensionSettings() {
|
export default function ExtensionSettings() {
|
||||||
// const { data: volumes } = useBridgeQuery('SysGetVolumes');
|
// const { data: volumes } = useBridgeQuery('GetVolumes');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<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 React from 'react';
|
||||||
|
|
||||||
|
import Card from '../../../components/layout/Card';
|
||||||
|
import { Toggle } from '../../../components/primitive';
|
||||||
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
import { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||||
|
|
||||||
export default function GeneralSettings() {
|
export default function GeneralSettings() {
|
||||||
// const { data: volumes } = useBridgeQuery('SysGetVolumes');
|
const { data: node } = useBridgeQuery('GetNode');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
|
@ -12,24 +17,53 @@ export default function GeneralSettings() {
|
||||||
title="General Settings"
|
title="General Settings"
|
||||||
description="General settings related to this client."
|
description="General settings related to this client."
|
||||||
/>
|
/>
|
||||||
{/* <InputContainer title="Volumes" description="A list of volumes running on this device.">
|
<Card className="px-5 dark:bg-gray-600">
|
||||||
<div className="flex flex-row space-x-2">
|
<div className="flex flex-col w-full my-2">
|
||||||
<div className="flex flex-grow">
|
<div className="flex">
|
||||||
<Listbox
|
<span className="font-semibold">Connected Node</span>
|
||||||
options={
|
<div className="flex-grow" />
|
||||||
volumes?.map((volume) => {
|
<div className="space-x-2">
|
||||||
const name = volume.name && volume.name.length ? volume.name : volume.mount_point;
|
<span className="px-2 py-[2px] rounded text-xs font-medium bg-gray-500">0 Peers</span>
|
||||||
return {
|
<span className="px-1.5 py-[2px] rounded text-xs font-medium bg-primary-600">
|
||||||
key: name,
|
Running
|
||||||
option: name,
|
</span>
|
||||||
description: volume.mount_point
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</InputContainer> */}
|
</Card>
|
||||||
</SettingsContainer>
|
</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-row pb-3 space-x-5">
|
||||||
<div className="flex flex-col flex-grow">
|
<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
|
<Input
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
@ -72,7 +72,9 @@ export default function LibraryGeneralSettings() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col flex-grow">
|
<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
|
<Input
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||||
// ];
|
// ];
|
||||||
|
|
||||||
export default function LocationSettings() {
|
export default function LocationSettings() {
|
||||||
const { data: locations } = useLibraryQuery('SysGetLocations');
|
const { data: locations } = useLibraryQuery('GetLocations');
|
||||||
|
|
||||||
const appProps = useContext(AppPropsContext);
|
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 { SettingsContainer } from '../../../components/settings/SettingsContainer';
|
||||||
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
import { SettingsHeader } from '../../../components/settings/SettingsHeader';
|
||||||
|
|
||||||
export default function TagsSettings() {
|
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 (
|
return (
|
||||||
<SettingsContainer>
|
<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>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ export default function LibrarySettings() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: libraries } = useBridgeQuery('NodeGetLibraries');
|
const { data: libraries } = useBridgeQuery('GetLibraries');
|
||||||
|
|
||||||
function createNewLib() {
|
function createNewLib() {
|
||||||
if (newLibName) {
|
if (newLibName) {
|
||||||
|
|
|
@ -130,3 +130,24 @@ body {
|
||||||
.dialog-content[data-state='closed'] {
|
.dialog-content[data-state='closed'] {
|
||||||
animation: bounceDown 100ms ease-in forwards;
|
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
|
prettier: ^2.6.2
|
||||||
pretty-bytes: ^6.0.0
|
pretty-bytes: ^6.0.0
|
||||||
react: ^18.1.0
|
react: ^18.1.0
|
||||||
|
react-colorful: ^5.5.1
|
||||||
react-countup: ^6.2.0
|
react-countup: ^6.2.0
|
||||||
react-dom: ^18.1.0
|
react-dom: ^18.1.0
|
||||||
react-dropzone: ^14.2.1
|
react-dropzone: ^14.2.1
|
||||||
react-error-boundary: ^3.1.4
|
react-error-boundary: ^3.1.4
|
||||||
|
react-hook-form: ^7.31.3
|
||||||
react-hotkeys-hook: ^3.4.6
|
react-hotkeys-hook: ^3.4.6
|
||||||
react-json-view: ^1.21.3
|
react-json-view: ^1.21.3
|
||||||
react-loading-icons: ^1.1.0
|
react-loading-icons: ^1.1.0
|
||||||
|
@ -347,10 +349,12 @@ importers:
|
||||||
phosphor-react: 1.4.1_react@18.1.0
|
phosphor-react: 1.4.1_react@18.1.0
|
||||||
pretty-bytes: 6.0.0
|
pretty-bytes: 6.0.0
|
||||||
react: 18.1.0
|
react: 18.1.0
|
||||||
|
react-colorful: 5.5.1_ef5jwxihqo6n7gxfmzogljlgcm
|
||||||
react-countup: 6.2.0_react@18.1.0
|
react-countup: 6.2.0_react@18.1.0
|
||||||
react-dom: 18.1.0_react@18.1.0
|
react-dom: 18.1.0_react@18.1.0
|
||||||
react-dropzone: 14.2.1_react@18.1.0
|
react-dropzone: 14.2.1_react@18.1.0
|
||||||
react-error-boundary: 3.1.4_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-hotkeys-hook: 3.4.6_ef5jwxihqo6n7gxfmzogljlgcm
|
||||||
react-json-view: 1.21.3_ohobp6rpsmerwlq5ipwfh5yigy
|
react-json-view: 1.21.3_ohobp6rpsmerwlq5ipwfh5yigy
|
||||||
react-loading-icons: 1.1.0
|
react-loading-icons: 1.1.0
|
||||||
|
@ -13910,6 +13914,16 @@ packages:
|
||||||
react: 18.1.0
|
react: 18.1.0
|
||||||
dev: false
|
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:
|
/react-countup/6.2.0_react@18.1.0:
|
||||||
resolution: {integrity: sha512-3WOKAQpWgjyFoH231SHEpIpHhDGb5g5EkTppM6T7vLa3X+8WMdw6750vVcY0wxysKiY00gTFhDwSB5qLU+xPZA==}
|
resolution: {integrity: sha512-3WOKAQpWgjyFoH231SHEpIpHhDGb5g5EkTppM6T7vLa3X+8WMdw6750vVcY0wxysKiY00gTFhDwSB5qLU+xPZA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
Loading…
Reference in a new issue