mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 12:13:27 +00:00
Location Settings (#302)
* Delete locations + Show online status * style tweaks * tweaks * location rescan button * fix location delete db locking bug * opting to remove self referencial relation on file_path * correct query * consolodate migration * consolodate migrations
This commit is contained in:
parent
7cb6e4f574
commit
2cc3f3d95d
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -17,6 +17,7 @@
|
||||||
"proptype",
|
"proptype",
|
||||||
"quicktime",
|
"quicktime",
|
||||||
"repr",
|
"repr",
|
||||||
|
"rescan",
|
||||||
"Roadmap",
|
"Roadmap",
|
||||||
"subpackage",
|
"subpackage",
|
||||||
"svgr",
|
"svgr",
|
||||||
|
|
|
@ -2,5 +2,8 @@
|
||||||
"name": "@sd/server",
|
"name": "@sd/server",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "GPL-3.0-only"
|
"license": "GPL-3.0-only",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cargo watch -x 'run -p server'"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ClientCommand = { key: "FileReadMetaData", params: { id: number, } } | { key: "FileSetNote", params: { id: number, note: string | null, } } | { key: "FileDelete", params: { id: number, } } | { key: "LibDelete", 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: "SysVolumeUnmount", params: { id: number, } } | { key: "GenerateThumbsForLocation", params: { id: number, path: string, } } | { key: "IdentifyUniqueFiles", params: { id: number, path: string, } };
|
export type ClientCommand = { key: "FileReadMetaData", params: { id: number, } } | { key: "FileSetNote", params: { id: number, note: string | null, } } | { key: "FileDelete", params: { id: number, } } | { key: "LibDelete", 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: "LocRescan", params: { id: number, } } | { key: "SysVolumeUnmount", params: { id: number, } } | { key: "GenerateThumbsForLocation", params: { id: number, path: string, } } | { key: "IdentifyUniqueFiles", params: { id: number, path: string, } };
|
|
@ -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 { JobStatus } from "./JobStatus";
|
import type { JobStatus } from "./JobStatus";
|
||||||
|
|
||||||
export interface JobReport { id: string, name: string, date_created: string, date_modified: string, status: JobStatus, task_count: number, completed_task_count: number, message: string, seconds_elapsed: string, }
|
export interface JobReport { id: string, name: string, data: string | null, date_created: string, date_modified: string, status: JobStatus, task_count: number, completed_task_count: number, message: string, seconds_elapsed: string, }
|
|
@ -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 { Platform } from "./Platform";
|
import type { Platform } from "./Platform";
|
||||||
|
|
||||||
export interface LibraryNode { uuid: string, name: string, platform: Platform, tcp_address: string, last_seen: string, last_synchronized: string, }
|
export interface LibraryNode { uuid: string, name: string, platform: Platform, last_seen: string, }
|
|
@ -1,3 +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 { LibraryNode } from "./LibraryNode";
|
||||||
|
|
||||||
export interface LocationResource { id: number, name: string | null, path: string | null, total_capacity: number | null, available_capacity: number | null, is_removable: boolean | null, is_online: boolean, date_created: string, }
|
export interface LocationResource { id: number, name: string | null, path: string | null, total_capacity: number | null, available_capacity: number | null, is_removable: boolean | null, node: LibraryNode | null, is_online: boolean, date_created: string, }
|
|
@ -0,0 +1,48 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "jobs" ADD COLUMN "data" TEXT;
|
||||||
|
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_locations" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"pub_id" TEXT NOT NULL,
|
||||||
|
"node_id" INTEGER,
|
||||||
|
"name" TEXT,
|
||||||
|
"local_path" TEXT,
|
||||||
|
"total_capacity" INTEGER,
|
||||||
|
"available_capacity" INTEGER,
|
||||||
|
"filesystem" TEXT,
|
||||||
|
"disk_type" INTEGER,
|
||||||
|
"is_removable" BOOLEAN,
|
||||||
|
"is_online" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "locations_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_locations" ("available_capacity", "date_created", "disk_type", "filesystem", "id", "is_online", "is_removable", "local_path", "name", "node_id", "pub_id", "total_capacity") SELECT "available_capacity", "date_created", "disk_type", "filesystem", "id", "is_online", "is_removable", "local_path", "name", "node_id", "pub_id", "total_capacity" FROM "locations";
|
||||||
|
DROP TABLE "locations";
|
||||||
|
ALTER TABLE "new_locations" RENAME TO "locations";
|
||||||
|
CREATE UNIQUE INDEX "locations_pub_id_key" ON "locations"("pub_id");
|
||||||
|
CREATE TABLE "new_file_paths" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"is_dir" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"location_id" INTEGER,
|
||||||
|
"materialized_path" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"extension" TEXT,
|
||||||
|
"file_id" INTEGER,
|
||||||
|
"parent_id" INTEGER,
|
||||||
|
"key_id" INTEGER,
|
||||||
|
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "file_paths_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "locations" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "file_paths_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "file_paths_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "keys" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_file_paths" ("date_created", "date_indexed", "date_modified", "extension", "file_id", "id", "is_dir", "key_id", "location_id", "materialized_path", "name", "parent_id") SELECT "date_created", "date_indexed", "date_modified", "extension", "file_id", "id", "is_dir", "key_id", "location_id", "materialized_path", "name", "parent_id" FROM "file_paths";
|
||||||
|
DROP TABLE "file_paths";
|
||||||
|
ALTER TABLE "new_file_paths" RENAME TO "file_paths";
|
||||||
|
CREATE INDEX "file_paths_location_id_idx" ON "file_paths"("location_id");
|
||||||
|
CREATE UNIQUE INDEX "file_paths_location_id_materialized_path_name_extension_key" ON "file_paths"("location_id", "materialized_path", "name", "extension");
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
|
@ -74,6 +74,7 @@ model Node {
|
||||||
sync_events SyncEvent[]
|
sync_events SyncEvent[]
|
||||||
jobs Job[]
|
jobs Job[]
|
||||||
|
|
||||||
|
Location Location[]
|
||||||
@@map("nodes")
|
@@map("nodes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,14 +108,14 @@ model Location {
|
||||||
is_online Boolean @default(true)
|
is_online Boolean @default(true)
|
||||||
date_created DateTime @default(now())
|
date_created DateTime @default(now())
|
||||||
|
|
||||||
|
node Node? @relation(fields: [node_id], references: [id])
|
||||||
file_paths FilePath[]
|
file_paths FilePath[]
|
||||||
@@map("locations")
|
@@map("locations")
|
||||||
}
|
}
|
||||||
|
|
||||||
model File {
|
model File {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
// content addressable storage id - sha256
|
// content addressable storage id - sha256 sampled checksum
|
||||||
// this does not need to be unique, as incoming replicas will always ignore if at least one exists
|
|
||||||
cas_id String @unique
|
cas_id String @unique
|
||||||
// full byte contents digested into sha256 checksum
|
// full byte contents digested into sha256 checksum
|
||||||
integrity_checksum String? @unique
|
integrity_checksum String? @unique
|
||||||
|
@ -157,7 +158,7 @@ model FilePath {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
is_dir Boolean @default(false)
|
is_dir Boolean @default(false)
|
||||||
// location that owns this path
|
// location that owns this path
|
||||||
location_id Int
|
location_id Int?
|
||||||
// a path generated from local file_path ids eg: "34/45/67/890"
|
// a path generated from local file_path ids eg: "34/45/67/890"
|
||||||
materialized_path String
|
materialized_path String
|
||||||
// the name and extension
|
// the name and extension
|
||||||
|
@ -177,12 +178,15 @@ model FilePath {
|
||||||
|
|
||||||
file File? @relation(fields: [file_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
file File? @relation(fields: [file_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
location Location? @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
location Location? @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
parent FilePath? @relation("directory_file_paths", fields: [parent_id], references: [id])
|
|
||||||
children FilePath[] @relation("directory_file_paths")
|
// NOTE: this self relation for the file tree was causing SQLite to go to forever bed, disabling until workaround
|
||||||
|
// parent FilePath? @relation("directory_file_paths", fields: [parent_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||||
|
// children FilePath[] @relation("directory_file_paths")
|
||||||
|
|
||||||
key Key? @relation(fields: [key_id], references: [id])
|
key Key? @relation(fields: [key_id], references: [id])
|
||||||
|
|
||||||
@@unique([location_id, materialized_path, name, extension])
|
@@unique([location_id, materialized_path, name, extension])
|
||||||
|
@@index([location_id])
|
||||||
@@map("file_paths")
|
@@map("file_paths")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,6 +291,7 @@ model Job {
|
||||||
node_id Int
|
node_id Int
|
||||||
action Int
|
action Int
|
||||||
status Int @default(0)
|
status Int @default(0)
|
||||||
|
data String?
|
||||||
|
|
||||||
task_count Int @default(1)
|
task_count Int @default(1)
|
||||||
completed_task_count Int @default(0)
|
completed_task_count Int @default(0)
|
||||||
|
|
|
@ -151,7 +151,7 @@ pub async fn get_images(
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<Vec<file_path::Data>, std::io::Error> {
|
) -> Result<Vec<file_path::Data>, std::io::Error> {
|
||||||
let mut params = vec![
|
let mut params = vec![
|
||||||
file_path::location_id::equals(location_id),
|
file_path::location_id::equals(Some(location_id)),
|
||||||
file_path::extension::in_vec(vec![
|
file_path::extension::in_vec(vec![
|
||||||
"png".to_string(),
|
"png".to_string(),
|
||||||
"jpeg".to_string(),
|
"jpeg".to_string(),
|
||||||
|
|
|
@ -159,7 +159,12 @@ impl Job for FileIdentifierJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle loop end
|
// handle loop end
|
||||||
let last_row = file_paths.last().unwrap();
|
let last_row = match file_paths.last() {
|
||||||
|
Some(l) => l,
|
||||||
|
None => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
cursor = last_row.id;
|
cursor = last_row.id;
|
||||||
completed += 1;
|
completed += 1;
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub async fn open_dir(
|
||||||
let directory = db
|
let directory = db
|
||||||
.file_path()
|
.file_path()
|
||||||
.find_first(vec![
|
.find_first(vec![
|
||||||
file_path::location_id::equals(location.id),
|
file_path::location_id::equals(Some(location.id)),
|
||||||
file_path::materialized_path::equals(path.into()),
|
file_path::materialized_path::equals(path.into()),
|
||||||
file_path::is_dir::equals(true),
|
file_path::is_dir::equals(true),
|
||||||
])
|
])
|
||||||
|
@ -35,7 +35,7 @@ pub async fn open_dir(
|
||||||
let mut file_paths: Vec<FilePath> = db
|
let mut file_paths: Vec<FilePath> = db
|
||||||
.file_path()
|
.file_path()
|
||||||
.find_many(vec![
|
.find_many(vec![
|
||||||
file_path::location_id::equals(location.id),
|
file_path::location_id::equals(Some(location.id)),
|
||||||
file_path::parent_id::equals(Some(directory.id)),
|
file_path::parent_id::equals(Some(directory.id)),
|
||||||
])
|
])
|
||||||
.with(file_path::file::fetch())
|
.with(file_path::file::fetch())
|
||||||
|
|
|
@ -110,7 +110,7 @@ impl Into<FilePath> for file_path::Data {
|
||||||
materialized_path: self.materialized_path,
|
materialized_path: self.materialized_path,
|
||||||
file_id: self.file_id,
|
file_id: self.file_id,
|
||||||
parent_id: self.parent_id,
|
parent_id: self.parent_id,
|
||||||
location_id: self.location_id,
|
location_id: self.location_id.unwrap_or(0),
|
||||||
date_indexed: self.date_indexed.into(),
|
date_indexed: self.date_indexed.into(),
|
||||||
name: self.name,
|
name: self.name,
|
||||||
extension: self.extension,
|
extension: self.extension,
|
||||||
|
|
|
@ -18,7 +18,8 @@ use std::{
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
const MAX_WORKERS: usize = 4;
|
// db is single threaded, nerd
|
||||||
|
const MAX_WORKERS: usize = 1;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait Job: Send + Sync + Debug {
|
pub trait Job: Send + Sync + Debug {
|
||||||
|
@ -40,6 +41,7 @@ impl Jobs {
|
||||||
running_workers: HashMap::new(),
|
running_workers: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ingest(&mut self, ctx: &CoreContext, job: Box<dyn Job>) {
|
pub async fn ingest(&mut self, ctx: &CoreContext, job: Box<dyn Job>) {
|
||||||
// create worker to process job
|
// create worker to process job
|
||||||
if self.running_workers.len() < MAX_WORKERS {
|
if self.running_workers.len() < MAX_WORKERS {
|
||||||
|
@ -57,6 +59,7 @@ impl Jobs {
|
||||||
self.job_queue.push_back(job);
|
self.job_queue.push_back(job);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ingest_queue(&mut self, _ctx: &CoreContext, job: Box<dyn Job>) {
|
pub fn ingest_queue(&mut self, _ctx: &CoreContext, job: Box<dyn Job>) {
|
||||||
self.job_queue.push_back(job);
|
self.job_queue.push_back(job);
|
||||||
}
|
}
|
||||||
|
@ -69,6 +72,7 @@ impl Jobs {
|
||||||
self.ingest(ctx, job).await;
|
self.ingest(ctx, job).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_running(&self) -> Vec<JobReport> {
|
pub async fn get_running(&self) -> Vec<JobReport> {
|
||||||
let mut ret = vec![];
|
let mut ret = vec![];
|
||||||
|
|
||||||
|
@ -78,6 +82,19 @@ impl Jobs {
|
||||||
}
|
}
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn queue_pending_job(ctx: &CoreContext) -> Result<(), JobError> {
|
||||||
|
let db = &ctx.database;
|
||||||
|
|
||||||
|
let next_job = db
|
||||||
|
.job()
|
||||||
|
.find_first(vec![job::status::equals(JobStatus::Queued.int_value())])
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_history(ctx: &CoreContext) -> Result<Vec<JobReport>, JobError> {
|
pub async fn get_history(ctx: &CoreContext) -> Result<Vec<JobReport>, JobError> {
|
||||||
let db = &ctx.database;
|
let db = &ctx.database;
|
||||||
let jobs = db
|
let jobs = db
|
||||||
|
@ -103,6 +120,7 @@ pub enum JobReportUpdate {
|
||||||
pub struct JobReport {
|
pub struct JobReport {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub data: Option<String>,
|
||||||
// client_id: i32,
|
// client_id: i32,
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
pub date_created: chrono::DateTime<chrono::Utc>,
|
pub date_created: chrono::DateTime<chrono::Utc>,
|
||||||
|
@ -131,6 +149,7 @@ impl Into<JobReport> for job::Data {
|
||||||
completed_task_count: self.completed_task_count,
|
completed_task_count: self.completed_task_count,
|
||||||
date_created: self.date_created.into(),
|
date_created: self.date_created.into(),
|
||||||
date_modified: self.date_modified.into(),
|
date_modified: self.date_modified.into(),
|
||||||
|
data: self.data,
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
seconds_elapsed: self.seconds_elapsed,
|
seconds_elapsed: self.seconds_elapsed,
|
||||||
}
|
}
|
||||||
|
@ -147,6 +166,7 @@ impl JobReport {
|
||||||
date_modified: chrono::Utc::now(),
|
date_modified: chrono::Utc::now(),
|
||||||
status: JobStatus::Queued,
|
status: JobStatus::Queued,
|
||||||
task_count: 0,
|
task_count: 0,
|
||||||
|
data: None,
|
||||||
completed_task_count: 0,
|
completed_task_count: 0,
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
seconds_elapsed: 0,
|
seconds_elapsed: 0,
|
||||||
|
@ -154,6 +174,13 @@ impl JobReport {
|
||||||
}
|
}
|
||||||
pub async fn create(&self, ctx: &CoreContext) -> Result<(), JobError> {
|
pub async fn create(&self, ctx: &CoreContext) -> Result<(), JobError> {
|
||||||
let config = get_nodestate();
|
let config = get_nodestate();
|
||||||
|
|
||||||
|
let mut params = Vec::new();
|
||||||
|
|
||||||
|
if let Some(_) = &self.data {
|
||||||
|
params.push(job::data::set(self.data.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
ctx.database
|
ctx.database
|
||||||
.job()
|
.job()
|
||||||
.create(
|
.create(
|
||||||
|
@ -161,7 +188,7 @@ impl JobReport {
|
||||||
job::name::set(self.name.clone()),
|
job::name::set(self.name.clone()),
|
||||||
job::action::set(1),
|
job::action::set(1),
|
||||||
job::nodes::link(node::id::equals(config.node_id)),
|
job::nodes::link(node::id::equals(config.node_id)),
|
||||||
vec![],
|
params,
|
||||||
)
|
)
|
||||||
.exec()
|
.exec()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -36,6 +36,10 @@ impl WorkerContext {
|
||||||
.send(WorkerEvent::Progressed(updates))
|
.send(WorkerEvent::Progressed(updates))
|
||||||
.unwrap_or(());
|
.unwrap_or(());
|
||||||
}
|
}
|
||||||
|
// save the job data to
|
||||||
|
// pub fn save_data () {
|
||||||
|
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// a worker is a dedicated thread that runs a single job
|
// a worker is a dedicated thread that runs a single job
|
||||||
|
|
|
@ -258,13 +258,11 @@ impl Node {
|
||||||
CoreResponse::Success(())
|
CoreResponse::Success(())
|
||||||
}
|
}
|
||||||
ClientCommand::LocDelete { id } => {
|
ClientCommand::LocDelete { id } => {
|
||||||
ctx.database
|
sys::delete_location(&ctx, id).await?;
|
||||||
.location()
|
CoreResponse::Success(())
|
||||||
.find_unique(location::id::equals(id))
|
}
|
||||||
.delete()
|
ClientCommand::LocRescan { id } => {
|
||||||
.exec()
|
sys::scan_location(&ctx, id, String::new());
|
||||||
.await?;
|
|
||||||
|
|
||||||
CoreResponse::Success(())
|
CoreResponse::Success(())
|
||||||
}
|
}
|
||||||
// CRUD for files
|
// CRUD for files
|
||||||
|
@ -374,6 +372,7 @@ pub enum ClientCommand {
|
||||||
LocCreate { path: String },
|
LocCreate { path: String },
|
||||||
LocUpdate { id: i32, name: Option<String> },
|
LocUpdate { id: i32, name: Option<String> },
|
||||||
LocDelete { id: i32 },
|
LocDelete { id: i32 },
|
||||||
|
LocRescan { id: i32 },
|
||||||
// System
|
// System
|
||||||
SysVolumeUnmount { id: i32 },
|
SysVolumeUnmount { id: i32 },
|
||||||
GenerateThumbsForLocation { id: i32, path: String },
|
GenerateThumbsForLocation { id: i32, path: String },
|
||||||
|
|
|
@ -19,11 +19,18 @@ pub struct LibraryNode {
|
||||||
pub uuid: String,
|
pub uuid: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub platform: Platform,
|
pub platform: Platform,
|
||||||
pub tcp_address: String,
|
|
||||||
#[ts(type = "string")]
|
|
||||||
pub last_seen: DateTime<Utc>,
|
pub last_seen: DateTime<Utc>,
|
||||||
#[ts(type = "string")]
|
}
|
||||||
pub last_synchronized: DateTime<Utc>,
|
|
||||||
|
impl Into<LibraryNode> for node::Data {
|
||||||
|
fn into(self) -> LibraryNode {
|
||||||
|
LibraryNode {
|
||||||
|
uuid: self.pub_id,
|
||||||
|
name: self.name,
|
||||||
|
platform: IntEnum::from_int(self.platform).unwrap(),
|
||||||
|
last_seen: self.last_seen.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(i32)]
|
#[repr(i32)]
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
encode::ThumbnailJob,
|
encode::ThumbnailJob,
|
||||||
file::{cas::FileIdentifierJob, indexer::IndexerJob},
|
file::{cas::FileIdentifierJob, indexer::IndexerJob},
|
||||||
node::get_nodestate,
|
node::{get_nodestate, LibraryNode},
|
||||||
prisma::location,
|
prisma::{file_path, location},
|
||||||
ClientQuery, CoreContext, CoreEvent,
|
ClientQuery, CoreContext, CoreEvent,
|
||||||
};
|
};
|
||||||
|
use prisma_client_rust::{raw, PrismaValue};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{fs, io, io::Write, path::Path};
|
use std::{fs, io, io::Write, path::Path};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
@ -21,13 +22,14 @@ pub struct LocationResource {
|
||||||
pub total_capacity: Option<i32>,
|
pub total_capacity: Option<i32>,
|
||||||
pub available_capacity: Option<i32>,
|
pub available_capacity: Option<i32>,
|
||||||
pub is_removable: Option<bool>,
|
pub is_removable: Option<bool>,
|
||||||
|
pub node: Option<LibraryNode>,
|
||||||
pub is_online: bool,
|
pub is_online: bool,
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
pub date_created: chrono::DateTime<chrono::Utc>,
|
pub date_created: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<LocationResource> for location::Data {
|
impl Into<LocationResource> for location::Data {
|
||||||
fn into(self) -> LocationResource {
|
fn into(mut self) -> LocationResource {
|
||||||
LocationResource {
|
LocationResource {
|
||||||
id: self.id,
|
id: self.id,
|
||||||
name: self.name,
|
name: self.name,
|
||||||
|
@ -35,6 +37,7 @@ impl Into<LocationResource> for location::Data {
|
||||||
total_capacity: self.total_capacity,
|
total_capacity: self.total_capacity,
|
||||||
available_capacity: self.available_capacity,
|
available_capacity: self.available_capacity,
|
||||||
is_removable: self.is_removable,
|
is_removable: self.is_removable,
|
||||||
|
node: self.node.take().unwrap_or(None).map(|node| (*node).into()),
|
||||||
is_online: self.is_online,
|
is_online: self.is_online,
|
||||||
date_created: self.date_created.into(),
|
date_created: self.date_created.into(),
|
||||||
}
|
}
|
||||||
|
@ -81,26 +84,24 @@ pub async fn get_location(
|
||||||
Ok(location.into())
|
Ok(location.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scan_location(ctx: &CoreContext, location_id: i32, path: String) {
|
||||||
|
ctx.spawn_job(Box::new(IndexerJob { path: path.clone() }));
|
||||||
|
ctx.queue_job(Box::new(FileIdentifierJob { location_id, path }));
|
||||||
|
// TODO: make a way to stop jobs so this can be canceled without rebooting app
|
||||||
|
// ctx.queue_job(Box::new(ThumbnailJob {
|
||||||
|
// location_id,
|
||||||
|
// path: "".to_string(),
|
||||||
|
// background: false,
|
||||||
|
// }));
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn new_location_and_scan(
|
pub async fn new_location_and_scan(
|
||||||
ctx: &CoreContext,
|
ctx: &CoreContext,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<LocationResource, SysError> {
|
) -> Result<LocationResource, SysError> {
|
||||||
let location = create_location(&ctx, path).await?;
|
let location = create_location(&ctx, path).await?;
|
||||||
|
|
||||||
ctx.spawn_job(Box::new(IndexerJob {
|
scan_location(&ctx, location.id, path.to_string());
|
||||||
path: path.to_string(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
ctx.queue_job(Box::new(FileIdentifierJob {
|
|
||||||
location_id: location.id,
|
|
||||||
path: path.to_string(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
ctx.queue_job(Box::new(ThumbnailJob {
|
|
||||||
location_id: location.id,
|
|
||||||
path: "".to_string(),
|
|
||||||
background: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
Ok(location)
|
Ok(location)
|
||||||
}
|
}
|
||||||
|
@ -108,7 +109,12 @@ pub async fn new_location_and_scan(
|
||||||
pub async fn get_locations(ctx: &CoreContext) -> Result<Vec<LocationResource>, SysError> {
|
pub async fn get_locations(ctx: &CoreContext) -> Result<Vec<LocationResource>, SysError> {
|
||||||
let db = &ctx.database;
|
let db = &ctx.database;
|
||||||
|
|
||||||
let locations = db.location().find_many(vec![]).exec().await?;
|
let locations = db
|
||||||
|
.location()
|
||||||
|
.find_many(vec![])
|
||||||
|
.with(location::node::fetch())
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
|
||||||
// turn locations into LocationResource
|
// turn locations into LocationResource
|
||||||
let locations: Vec<LocationResource> = locations
|
let locations: Vec<LocationResource> = locations
|
||||||
|
@ -175,6 +181,7 @@ pub async fn create_location(ctx: &CoreContext, path: &str) -> Result<LocationRe
|
||||||
)),
|
)),
|
||||||
location::is_online::set(true),
|
location::is_online::set(true),
|
||||||
location::local_path::set(Some(path.to_string())),
|
location::local_path::set(Some(path.to_string())),
|
||||||
|
location::node_id::set(Some(config.node_id)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.exec()
|
.exec()
|
||||||
|
@ -213,6 +220,29 @@ pub async fn create_location(ctx: &CoreContext, path: &str) -> Result<LocationRe
|
||||||
Ok(location.into())
|
Ok(location.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_location(ctx: &CoreContext, location_id: i32) -> Result<(), SysError> {
|
||||||
|
let db = &ctx.database;
|
||||||
|
|
||||||
|
db.file_path()
|
||||||
|
.find_many(vec![file_path::location_id::equals(Some(location_id))])
|
||||||
|
.delete()
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
db.location()
|
||||||
|
.find_unique(location::id::equals(location_id))
|
||||||
|
.delete()
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
ctx.emit(CoreEvent::InvalidateQuery(ClientQuery::SysGetLocations))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
println!("Location {} deleted", location_id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum LocationError {
|
pub enum LocationError {
|
||||||
#[error("Failed to create location (uuid {uuid:?})")]
|
#[error("Failed to create location (uuid {uuid:?})")]
|
||||||
|
|
|
@ -19,7 +19,7 @@ export function AppLayout() {
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex flex-row h-screen overflow-hidden text-gray-900 select-none dark:text-white',
|
'flex flex-row h-screen overflow-hidden text-gray-900 select-none dark:text-white cursor-default',
|
||||||
isWindowRounded && 'rounded-xl',
|
isWindowRounded && 'rounded-xl',
|
||||||
hasWindowBorder && 'border border-gray-200 dark:border-gray-500'
|
hasWindowBorder && 'border border-gray-200 dark:border-gray-500'
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import React, { useState } from 'react';
|
||||||
import { Rings } from 'react-loading-icons';
|
import { Rings } from 'react-loading-icons';
|
||||||
|
|
||||||
import FileItem from '../file/FileItem';
|
import FileItem from '../file/FileItem';
|
||||||
|
import Loader from '../primitive/Loader';
|
||||||
import ProgressBar from '../primitive/ProgressBar';
|
import ProgressBar from '../primitive/ProgressBar';
|
||||||
|
|
||||||
export interface DeviceProps {
|
export interface DeviceProps {
|
||||||
|
@ -45,13 +46,7 @@ export function Device(props: DeviceProps) {
|
||||||
<div className="flex flex-grow" />
|
<div className="flex flex-grow" />
|
||||||
{props.runningJob && (
|
{props.runningJob && (
|
||||||
<div className="flex flex-row ml-5 bg-gray-300 bg-opacity-50 rounded-md dark:bg-gray-550">
|
<div className="flex flex-row ml-5 bg-gray-300 bg-opacity-50 rounded-md dark:bg-gray-550">
|
||||||
<Rings
|
<Loader />
|
||||||
stroke="#2599FF"
|
|
||||||
strokeOpacity={4}
|
|
||||||
strokeWidth={10}
|
|
||||||
speed={0.5}
|
|
||||||
className="ml-0.5 mt-[2px] -mr-1 w-7 h-7"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col p-2">
|
<div className="flex flex-col p-2">
|
||||||
<span className="mb-[2px] -mt-1 truncate text-gray-450 text-tiny">
|
<span className="mb-[2px] -mt-1 truncate text-gray-450 text-tiny">
|
||||||
{props.runningJob.task}...
|
{props.runningJob.task}...
|
||||||
|
|
|
@ -35,20 +35,20 @@ export const Inspector = (props: {
|
||||||
location?: LocationResource;
|
location?: LocationResource;
|
||||||
selectedFile?: FilePath;
|
selectedFile?: FilePath;
|
||||||
}) => {
|
}) => {
|
||||||
const file_path = props.selectedFile;
|
const file_path = props.selectedFile,
|
||||||
let full_path = `${props.location?.path}/${file_path?.materialized_path}`;
|
full_path = `${props.location?.path}/${file_path?.materialized_path}`,
|
||||||
|
file_id = props.selectedFile?.file?.id || -1;
|
||||||
|
|
||||||
// notes are stored in global state by their file id
|
// notes are cached in a store by their file id
|
||||||
// this is so we can ensure every note has been sent to Rust even
|
// this is so we can ensure every note has been sent to Rust even
|
||||||
// when quickly navigating files, which cancels update function
|
// when quickly navigating files, which cancels update function
|
||||||
const { notes, setNote, unCacheNote } = useInspectorState();
|
const { notes, setNote, unCacheNote } = useInspectorState();
|
||||||
|
|
||||||
const file_id = props.selectedFile?.file?.id || -1;
|
// show cached note over server note, important to check for undefined not falsey
|
||||||
// show cached note over server note
|
|
||||||
const note =
|
const note =
|
||||||
notes[file_id] === undefined ? props.selectedFile?.file?.note || null : notes[file_id];
|
notes[file_id] === undefined ? props.selectedFile?.file?.note || null : notes[file_id];
|
||||||
|
|
||||||
// when input is updated
|
// when input is updated, cache note
|
||||||
function handleNoteUpdate(e: React.ChangeEvent<HTMLTextAreaElement>) {
|
function handleNoteUpdate(e: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||||
if (e.target.value !== note) {
|
if (e.target.value !== note) {
|
||||||
setNote(file_id, e.target.value);
|
setNote(file_id, e.target.value);
|
||||||
|
@ -63,19 +63,19 @@ export const Inspector = (props: {
|
||||||
}, [note]);
|
}, [note]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition
|
// <Transition
|
||||||
as={React.Fragment}
|
// as={React.Fragment}
|
||||||
show={true}
|
// show={true}
|
||||||
enter="transition-translate ease-in-out duration-200"
|
// enter="transition-translate ease-in-out duration-200"
|
||||||
enterFrom="translate-x-64"
|
// enterFrom="translate-x-64"
|
||||||
enterTo="translate-x-0"
|
// enterTo="translate-x-0"
|
||||||
leave="transition-translate ease-in-out duration-200"
|
// leave="transition-translate ease-in-out duration-200"
|
||||||
leaveFrom="translate-x-0"
|
// leaveFrom="translate-x-0"
|
||||||
leaveTo="translate-x-64"
|
// leaveTo="translate-x-64"
|
||||||
>
|
// >
|
||||||
<div className="flex p-2 pr-1 mr-1 pb-[51px] w-72 flex-wrap overflow-x-hidden custom-scroll inspector-scroll">
|
<div className="flex p-2 pr-1 mr-1 pb-[51px] w-[330px] flex-wrap overflow-x-hidden custom-scroll inspector-scroll">
|
||||||
{!!file_path && (
|
{!!file_path && (
|
||||||
<div className="flex flex-col pb-2 overflow-hidden bg-white rounded-lg select-text dark:bg-gray-600 bg-opacity-70">
|
<div className="flex flex-col w-full pb-2 overflow-hidden bg-white rounded-lg select-text dark:bg-gray-600 bg-opacity-70">
|
||||||
<div className="flex items-center justify-center w-full h-64 overflow-hidden rounded-t-lg bg-gray-50 dark:bg-gray-900">
|
<div className="flex items-center justify-center w-full h-64 overflow-hidden rounded-t-lg bg-gray-50 dark:bg-gray-900">
|
||||||
<FileThumb
|
<FileThumb
|
||||||
hasThumbnailOverride={false}
|
hasThumbnailOverride={false}
|
||||||
|
@ -152,6 +152,6 @@ export const Inspector = (props: {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
// </Transition>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -93,7 +93,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex flex-col flex-grow-0 flex-shrink-0 w-48 min-h-full px-2.5 overflow-x-hidden overflow-y-scroll border-r border-gray-100 no-scrollbar bg-gray-50 dark:bg-gray-850 dark:border-gray-600',
|
'flex flex-col flex-grow-0 flex-shrink-0 w-48 min-h-full px-2.5 overflow-x-hidden overflow-y-scroll border-r border-gray-100 no-scrollbar bg-gray-50 dark:bg-gray-850 dark:border-gray-750',
|
||||||
{
|
{
|
||||||
'dark:!bg-opacity-40': appProps?.platform === 'macOS'
|
'dark:!bg-opacity-40': appProps?.platform === 'macOS'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
import { Button } from '@sd/ui';
|
import { Button } from '@sd/ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import Loader from '../primitive/Loader';
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
trigger: ReactNode;
|
trigger: ReactNode;
|
||||||
ctaLabel?: string;
|
ctaLabel?: string;
|
||||||
|
ctaDanger?: boolean;
|
||||||
ctaAction?: () => void;
|
ctaAction?: () => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
children: ReactNode;
|
children?: ReactNode;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Dialog(props: DialogProps) {
|
export default function Dialog(props: DialogProps) {
|
||||||
|
@ -26,12 +31,21 @@ export default function Dialog(props: DialogProps) {
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-end px-3 py-3 space-x-2 bg-gray-600 border-t border-gray-550">
|
<div className="flex flex-row justify-end px-3 py-3 space-x-2 bg-gray-600 border-t border-gray-550">
|
||||||
|
{props.loading && <Loader />}
|
||||||
|
<div className="flex-grow" />
|
||||||
<DialogPrimitive.Close asChild>
|
<DialogPrimitive.Close asChild>
|
||||||
<Button size="sm" variant="gray">
|
<Button loading={props.loading} disabled={props.loading} size="sm" variant="gray">
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
<Button onClick={props.ctaAction} size="sm" variant="primary">
|
<Button
|
||||||
|
onClick={props.ctaAction}
|
||||||
|
size="sm"
|
||||||
|
loading={props.loading}
|
||||||
|
disabled={props.loading}
|
||||||
|
variant={props.ctaDanger ? 'colored' : 'primary'}
|
||||||
|
className={clsx(props.ctaDanger && 'bg-red-500 border-red-500')}
|
||||||
|
>
|
||||||
{props.ctaLabel}
|
{props.ctaLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -104,7 +104,7 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
|
||||||
<TopBarButton
|
<TopBarButton
|
||||||
icon={ArrowsClockwise}
|
icon={ArrowsClockwise}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
generateThumbsForLocation({ id: locationId, path: '' });
|
// generateThumbsForLocation({ id: locationId, path: '' });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { DotsVerticalIcon, RefreshIcon } from '@heroicons/react/outline';
|
||||||
|
import { CogIcon, TrashIcon } from '@heroicons/react/solid';
|
||||||
|
import { command, useBridgeCommand } from '@sd/client';
|
||||||
|
import { LocationResource } from '@sd/core';
|
||||||
|
import { Button } from '@sd/ui';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { Folder } from '../icons/Folder';
|
||||||
|
import Dialog from '../layout/Dialog';
|
||||||
|
|
||||||
|
interface LocationListItemProps {
|
||||||
|
location: LocationResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LocationListItem({ location }: LocationListItemProps) {
|
||||||
|
const [hide, setHide] = useState(false);
|
||||||
|
|
||||||
|
const { mutate: locRescan } = useBridgeCommand('LocRescan');
|
||||||
|
|
||||||
|
const { mutate: deleteLoc, isLoading: locDeletePending } = useBridgeCommand('LocDelete', {
|
||||||
|
onSuccess: () => {
|
||||||
|
setHide(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hide) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full px-4 py-2 border border-gray-500 rounded-lg bg-gray-550">
|
||||||
|
<DotsVerticalIcon className="w-5 h-5 mt-3 mr-1 -ml-3 cursor-move drag-handle opacity-10" />
|
||||||
|
<Folder size={30} className="mr-3" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h1 className="pt-0.5 text-sm font-semibold">{location.name}</h1>
|
||||||
|
<p className="mt-0.5 text-sm select-text text-gray-250">
|
||||||
|
<span className="py-[1px] px-1 bg-gray-500 rounded mr-1">{location.node?.name}</span>
|
||||||
|
{location.path}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-grow" />
|
||||||
|
<div className="flex h-[45px] p-2 space-x-2">
|
||||||
|
<Button disabled variant="gray" className="!py-1.5 !px-2 pointer-events-none flex">
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'w-2 h-2 rounded-full',
|
||||||
|
location.is_online ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="ml-1.5 text-xs text-gray-350">
|
||||||
|
{location.is_online ? 'Online' : 'Offline'}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
</Button>
|
||||||
|
<Dialog
|
||||||
|
title="Delete Location"
|
||||||
|
description="Deleting a location will also remove all files associated with it from the Spacedrive database, the files themselves will not be deleted."
|
||||||
|
ctaAction={() => {
|
||||||
|
deleteLoc({ id: location.id });
|
||||||
|
}}
|
||||||
|
loading={locDeletePending}
|
||||||
|
ctaDanger
|
||||||
|
ctaLabel="Delete"
|
||||||
|
trigger={
|
||||||
|
<Button variant="gray" className="!p-1.5">
|
||||||
|
<TrashIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="gray"
|
||||||
|
className="!p-1.5"
|
||||||
|
onClick={() => {
|
||||||
|
// this should cause a lite directory rescan, but this will do for now, so the button does something useful
|
||||||
|
locRescan({ id: location.id });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
{/* <Button variant="gray" className="!p-1.5">
|
||||||
|
<CogIcon className="w-4 h-4" />
|
||||||
|
</Button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
15
packages/interface/src/components/primitive/Loader.tsx
Normal file
15
packages/interface/src/components/primitive/Loader.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
import { Puff } from 'react-loading-icons';
|
||||||
|
|
||||||
|
export default function Loader(props: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<Puff
|
||||||
|
stroke="#2599FF"
|
||||||
|
strokeOpacity={4}
|
||||||
|
strokeWidth={5}
|
||||||
|
speed={1}
|
||||||
|
className={clsx('ml-0.5 mt-[2px] -mr-1 w-7 h-7', props.className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -17,8 +17,9 @@ export const DebugScreen: React.FC<{}> = (props) => {
|
||||||
// });
|
// });
|
||||||
const { mutate: identifyFiles } = useBridgeCommand('IdentifyUniqueFiles');
|
const { mutate: identifyFiles } = useBridgeCommand('IdentifyUniqueFiles');
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full h-screen p-5 custom-scroll page-scroll">
|
<div className="flex flex-col w-full h-screen custom-scroll page-scroll">
|
||||||
<div className="flex flex-col space-y-5 pb-7">
|
<div data-tauri-drag-region className="flex flex-shrink-0 w-full h-5" />
|
||||||
|
<div className="flex flex-col p-5 pt-2 space-y-5 pb-7">
|
||||||
<h1 className="text-lg font-bold ">Developer Debugger</h1>
|
<h1 className="text-lg font-bold ">Developer Debugger</h1>
|
||||||
<div className="flex flex-row pb-4 space-x-2">
|
<div className="flex flex-row pb-4 space-x-2">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { useBridgeQuery } from '@sd/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import LocationListItem from '../../components/location/LocationListItem';
|
||||||
import { InputContainer } from '../../components/primitive/InputContainer';
|
import { InputContainer } from '../../components/primitive/InputContainer';
|
||||||
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
import { SettingsContainer } from '../../components/settings/SettingsContainer';
|
||||||
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||||
|
@ -11,18 +13,20 @@ import { SettingsHeader } from '../../components/settings/SettingsHeader';
|
||||||
// ];
|
// ];
|
||||||
|
|
||||||
export default function LocationSettings() {
|
export default function LocationSettings() {
|
||||||
// const locations = useBridgeQuery("SysGetLocation")
|
const { data: locations } = useBridgeQuery('SysGetLocations');
|
||||||
|
|
||||||
|
console.log({ locations });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
{/*<Button size="sm">Add Location</Button>*/}
|
{/*<Button size="sm">Add Location</Button>*/}
|
||||||
<SettingsHeader title="Locations" description="Manage your settings related to locations." />
|
<SettingsHeader title="Locations" description="Manage your storage locations." />
|
||||||
<InputContainer
|
|
||||||
title="Something about a vault"
|
<div className="grid space-y-2">
|
||||||
description="Local cache storage for media previews and thumbnails."
|
{locations?.map((location) => (
|
||||||
>
|
<LocationListItem key={location.id} location={location} />
|
||||||
<div className="flex flex-row space-x-2"></div>
|
))}
|
||||||
</InputContainer>
|
</div>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,14 +40,12 @@ const variants = {
|
||||||
dark:hover:bg-gray-500
|
dark:hover:bg-gray-500
|
||||||
dark:bg-opacity-80
|
dark:bg-opacity-80
|
||||||
dark:hover:bg-opacity-100
|
dark:hover:bg-opacity-100
|
||||||
dark:active:bg-gray-550
|
|
||||||
dark:active:opacity-80
|
dark:active:opacity-80
|
||||||
|
|
||||||
border-gray-200
|
border-gray-200
|
||||||
hover:border-gray-300
|
hover:border-gray-300
|
||||||
active:border-gray-200
|
active:border-gray-200
|
||||||
dark:border-gray-500
|
dark:border-gray-500
|
||||||
dark:active:border-gray-600
|
|
||||||
dark:hover:border-gray-500
|
dark:hover:border-gray-500
|
||||||
|
|
||||||
text-gray-700
|
text-gray-700
|
||||||
|
@ -68,6 +66,12 @@ const variants = {
|
||||||
hover:border-primary-500
|
hover:border-primary-500
|
||||||
active:border-primary-700
|
active:border-primary-700
|
||||||
`,
|
`,
|
||||||
|
colored: `
|
||||||
|
text-white
|
||||||
|
shadow-sm
|
||||||
|
hover:bg-opacity-90
|
||||||
|
active:bg-opacity-100
|
||||||
|
`,
|
||||||
selected: `bg-gray-100 dark:bg-gray-500
|
selected: `bg-gray-100 dark:bg-gray-500
|
||||||
text-black hover:text-black active:text-black dark:hover:text-white dark:text-white
|
text-black hover:text-black active:text-black dark:hover:text-white dark:text-white
|
||||||
`
|
`
|
||||||
|
@ -114,7 +118,7 @@ export const Button = forwardRef<
|
||||||
) => {
|
) => {
|
||||||
className = clsx(
|
className = clsx(
|
||||||
'border rounded-md items-center transition-colors duration-100 cursor-default',
|
'border rounded-md items-center transition-colors duration-100 cursor-default',
|
||||||
{ 'opacity-5': loading, '!p-1': noPadding },
|
{ 'opacity-70': loading, '!p-1': noPadding },
|
||||||
{ 'justify-center': !justifyLeft },
|
{ 'justify-center': !justifyLeft },
|
||||||
sizes[size || 'default'],
|
sizes[size || 'default'],
|
||||||
variants[variant || 'default'],
|
variants[variant || 'default'],
|
||||||
|
|
Loading…
Reference in a new issue