diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs
index 8ae8ba8e3..b773aa813 100644
--- a/apps/desktop/src-tauri/src/main.rs
+++ b/apps/desktop/src-tauri/src/main.rs
@@ -1,3 +1,8 @@
+#![cfg_attr(
+ all(not(debug_assertions), target_os = "windows"),
+ windows_subsystem = "windows"
+)]
+
use std::path::PathBuf;
use sdcore::Node;
diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json
index 7c5bfc74c..ef43c8130 100644
--- a/apps/desktop/src-tauri/tauri.conf.json
+++ b/apps/desktop/src-tauri/tauri.conf.json
@@ -71,6 +71,7 @@
"fullscreen": false,
"alwaysOnTop": false,
"focus": false,
+ "visible": false,
"fileDropEnabled": false,
"decorations": true,
"transparent": true,
diff --git a/apps/desktop/src/index.html b/apps/desktop/src/index.html
index 4d441a0c7..b51fcedeb 100644
--- a/apps/desktop/src/index.html
+++ b/apps/desktop/src/index.html
@@ -1,5 +1,5 @@
-
+
diff --git a/apps/mobile/ios/Spacedrive/Info.plist b/apps/mobile/ios/Spacedrive/Info.plist
index 1d07fc916..fd2881b25 100644
--- a/apps/mobile/ios/Spacedrive/Info.plist
+++ b/apps/mobile/ios/Spacedrive/Info.plist
@@ -1,83 +1,83 @@
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Spacedrive
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- $(PRODUCT_BUNDLE_PACKAGE_TYPE)
- CFBundleShortVersionString
- 0.0.1
- CFBundleSignature
- ????
- CFBundleURLTypes
-
-
- CFBundleURLSchemes
-
- spacedrive
- com.spacedrive.app
-
-
-
- CFBundleVersion
- 1
- LSRequiresIPhoneOS
-
- NSAppTransportSecurity
- NSAllowsArbitraryLoads
-
- NSExceptionDomains
-
- localhost
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Spacedrive
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 0.0.1
+ CFBundleSignature
+ ????
+ CFBundleURLTypes
+
- NSExceptionAllowsInsecureHTTPLoads
-
+ CFBundleURLSchemes
+
+ spacedrive
+ com.spacedrive.app
+
+
+
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+ NSExceptionDomains
+
+ localhost
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+
+ UIBackgroundModes
+
+ remote-notification
+
+ UIFileSharingEnabled
+
+ UILaunchStoryboardName
+ SplashScreen
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UIRequiresFullScreen
+
+ UIStatusBarStyle
+ UIStatusBarStyleDefault
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIUserInterfaceStyle
+ Automatic
+ UIViewControllerBasedStatusBarAppearance
+
- UIBackgroundModes
-
- remote-notification
-
- UIFileSharingEnabled
-
- UILaunchStoryboardName
- SplashScreen
- UIRequiredDeviceCapabilities
-
- armv7
-
- UIRequiresFullScreen
-
- UIStatusBarStyle
- UIStatusBarStyleDefault
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIUserInterfaceStyle
- Automatic
- UIViewControllerBasedStatusBarAppearance
-
-
-
+
\ No newline at end of file
diff --git a/apps/mobile/src/screens/Location.tsx b/apps/mobile/src/screens/Location.tsx
index 08c267ddd..2dd2525e7 100644
--- a/apps/mobile/src/screens/Location.tsx
+++ b/apps/mobile/src/screens/Location.tsx
@@ -6,8 +6,8 @@ import { SharedScreenProps } from '~/navigation/SharedScreens';
export default function LocationScreen({ navigation, route }: SharedScreenProps<'Location'>) {
const { id } = route.params;
return (
-
- Location {id}
+
+ Location {id}
);
}
diff --git a/apps/mobile/src/screens/Spaces.tsx b/apps/mobile/src/screens/Spaces.tsx
index 706105973..10c8cc5b9 100644
--- a/apps/mobile/src/screens/Spaces.tsx
+++ b/apps/mobile/src/screens/Spaces.tsx
@@ -5,8 +5,8 @@ import { SpacesStackScreenProps } from '~/navigation/tabs/SpacesStack';
export default function SpacesScreen({ navigation }: SpacesStackScreenProps<'Spaces'>) {
return (
-
- Spaces
+
+ Spaces
);
}
diff --git a/core/prisma/migrations/20220902014900_add_is_archived/migration.sql b/core/prisma/migrations/20220902014900_add_is_archived/migration.sql
new file mode 100644
index 000000000..fa390a498
--- /dev/null
+++ b/core/prisma/migrations/20220902014900_add_is_archived/migration.sql
@@ -0,0 +1,28 @@
+-- AlterTable
+ALTER TABLE "files" ADD COLUMN "extension" TEXT;
+ALTER TABLE "files" ADD COLUMN "name" TEXT;
+
+-- RedefineTables
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_locations" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "pub_id" BLOB 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,
+ "is_archived" BOOLEAN NOT NULL DEFAULT false,
+ "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");
+PRAGMA foreign_key_check;
+PRAGMA foreign_keys=ON;
diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma
index 7bff3a98d..fbf71b6be 100644
--- a/core/prisma/schema.prisma
+++ b/core/prisma/schema.prisma
@@ -32,6 +32,7 @@ model SyncEvent {
value String
node Node @relation(fields: [node_id], references: [id])
+
@@map("sync_events")
}
@@ -51,7 +52,7 @@ model Statistics {
model Node {
id Int @id @default(autoincrement())
- pub_id Bytes @unique
+ pub_id Bytes @unique
name String
platform Int @default(0)
version String?
@@ -63,6 +64,7 @@ model Node {
jobs Job[]
Location Location[]
+
@@map("nodes")
}
@@ -94,13 +96,13 @@ model Location {
disk_type Int?
is_removable Boolean?
is_online Boolean @default(true)
+ is_archived Boolean @default(false)
date_created DateTime @default(now())
- node Node? @relation(fields: [node_id], references: [id])
- file_paths FilePath[]
+ node Node? @relation(fields: [node_id], references: [id])
+ file_paths FilePath[]
indexer_rules IndexerRulesInLocation[]
- @@unique([node_id, local_path])
@@map("locations")
}
@@ -111,6 +113,8 @@ model File {
// full byte contents digested into sha256 checksum
integrity_checksum String? @unique
// basic metadata
+ name String?
+ extension String?
kind Int @default(0)
size_in_bytes String
key_id Int?
@@ -205,6 +209,7 @@ model Key {
files File[]
file_paths FilePath[]
+
@@map("keys")
}
@@ -230,7 +235,7 @@ model MediaData {
model Tag {
id Int @id @default(autoincrement())
- pub_id Bytes @unique
+ pub_id Bytes @unique
name String?
color String?
total_files Int? @default(0)
@@ -239,6 +244,7 @@ model Tag {
date_modified DateTime @default(now())
tag_files TagOnFile[]
+
@@map("tags")
}
@@ -257,12 +263,13 @@ model TagOnFile {
model Label {
id Int @id @default(autoincrement())
- pub_id Bytes @unique
+ pub_id Bytes @unique
name String?
date_created DateTime @default(now())
date_modified DateTime @default(now())
label_files LabelOnFile[]
+
@@map("labels")
}
@@ -281,13 +288,14 @@ model LabelOnFile {
model Space {
id Int @id @default(autoincrement())
- pub_id Bytes @unique
+ pub_id Bytes @unique
name String?
description String?
date_created DateTime @default(now())
date_modified DateTime @default(now())
files FileInSpace[]
+
@@map("spaces")
}
@@ -309,7 +317,7 @@ model Job {
name String
node_id Int
action Int
- status Int @default(0)
+ status Int @default(0)
data Bytes?
task_count Int @default(1)
@@ -319,12 +327,13 @@ model Job {
seconds_elapsed Int @default(0)
nodes Node @relation(fields: [node_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
+
@@map("jobs")
}
model Album {
id Int @id @default(autoincrement())
- pub_id Bytes @unique
+ pub_id Bytes @unique
name String
is_hidden Boolean @default(false)
@@ -351,7 +360,7 @@ model FileInAlbum {
model Comment {
id Int @id @default(autoincrement())
- pub_id Bytes @unique
+ pub_id Bytes @unique
content String
date_created DateTime @default(now())
date_modified DateTime @default(now())
@@ -375,14 +384,14 @@ model IndexerRule {
}
model IndexerRulesInLocation {
- date_created DateTime @default(now())
+ date_created DateTime @default(now())
- location_id Int
- location Location @relation(fields: [location_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
+ location_id Int
+ location Location @relation(fields: [location_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
indexer_rule_id Int
indexer_rule IndexerRule @relation(fields: [indexer_rule_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
@@id([location_id, indexer_rule_id])
@@map("indexer_rules_in_location")
-}
\ No newline at end of file
+}
diff --git a/core/src/api/files.rs b/core/src/api/files.rs
index 3c1728b20..bf6f749b5 100644
--- a/core/src/api/files.rs
+++ b/core/src/api/files.rs
@@ -1,4 +1,4 @@
-use crate::{api::locations::GetExplorerDirArgs, invalidate_query, prisma::file};
+use crate::{api::locations::LocationExplorerArgs, invalidate_query, prisma::file};
use rspc::Type;
use serde::Deserialize;
@@ -30,7 +30,7 @@ pub(crate) fn mount() -> RouterBuilder {
.exec()
.await?;
- invalidate_query!(library, "locations.getExplorerDir");
+ invalidate_query!(library, "locations.getExplorerData");
Ok(())
})
@@ -51,14 +51,15 @@ pub(crate) fn mount() -> RouterBuilder {
invalidate_query!(
library,
- "locations.getExplorerDir": LibraryArgs,
+ "locations.getExplorerData": LibraryArgs,
LibraryArgs {
library_id: library.id,
- arg: GetExplorerDirArgs {
+ arg: LocationExplorerArgs {
// TODO: Set these arguments to the correct type
location_id: 0,
path: "".into(),
limit: 0,
+ cursor: None,
}
}
);
@@ -78,14 +79,15 @@ pub(crate) fn mount() -> RouterBuilder {
invalidate_query!(
library,
- "locations.getExplorerDir": LibraryArgs,
+ "locations.getExplorerData": LibraryArgs,
LibraryArgs {
library_id: library.id,
- arg: GetExplorerDirArgs {
+ arg: LocationExplorerArgs {
// TODO: Set these arguments to the correct type
location_id: 0,
path: "".into(),
limit: 0,
+ cursor: None,
}
}
);
diff --git a/core/src/api/jobs.rs b/core/src/api/jobs.rs
index d2f54143e..773c3ee26 100644
--- a/core/src/api/jobs.rs
+++ b/core/src/api/jobs.rs
@@ -77,6 +77,7 @@ pub(crate) fn mount() -> RouterBuilder {
.exec()
.await?
.ok_or(LocationError::IdNotFound(args.id))?,
+ sub_path: Some(args.path),
},
Box::new(FileIdentifierJob {}),
))
diff --git a/core/src/api/locations.rs b/core/src/api/locations.rs
index 43c901374..cf8345303 100644
--- a/core/src/api/locations.rs
+++ b/core/src/api/locations.rs
@@ -5,7 +5,7 @@ use crate::{
fetch_location, indexer::indexer_rules::IndexerRuleCreateArgs, scan_location,
with_indexer_rules, LocationCreateArgs, LocationError, LocationUpdateArgs,
},
- prisma::{file_path, indexer_rule, indexer_rules_in_location, location},
+ prisma::{file, file_path, indexer_rule, indexer_rules_in_location, location, tag},
};
use rspc::{self, ErrorCode, Type};
@@ -15,16 +15,32 @@ use tracing::info;
use super::{LibraryArgs, RouterBuilder};
#[derive(Serialize, Deserialize, Type, Debug)]
-pub struct DirectoryWithContents {
- pub directory: file_path::Data,
- pub contents: Vec,
+pub struct ExplorerData {
+ pub context: ExplorerContext,
+ pub items: Vec,
+}
+
+#[derive(Serialize, Deserialize, Type, Debug)]
+#[serde(tag = "type")]
+pub enum ExplorerContext {
+ Location(location::Data),
+ Tag(tag::Data),
+ // Space(object_in_space::Data),
+}
+
+#[derive(Serialize, Deserialize, Type, Debug)]
+#[serde(tag = "type")]
+pub enum ExplorerItem {
+ Path(Box),
+ Object(Box),
}
#[derive(Clone, Serialize, Deserialize, Type, Debug)]
-pub struct GetExplorerDirArgs {
+pub struct LocationExplorerArgs {
pub location_id: i32,
pub path: String,
pub limit: i32,
+ pub cursor: Option,
}
pub(crate) fn mount() -> RouterBuilder {
@@ -53,8 +69,8 @@ pub(crate) fn mount() -> RouterBuilder {
.await?)
})
.query(
- "getExplorerDir",
- |ctx, arg: LibraryArgs| async move {
+ "getExplorerData",
+ |ctx, arg: LibraryArgs| async move {
let (args, library) = arg.get_library(&ctx).await?;
let location = library
@@ -63,7 +79,9 @@ pub(crate) fn mount() -> RouterBuilder {
.find_unique(location::id::equals(args.location_id))
.exec()
.await?
- .unwrap();
+ .ok_or_else(|| {
+ rspc::Error::new(ErrorCode::NotFound, "Location not found".into())
+ })?;
let directory = library
.db
@@ -90,9 +108,9 @@ pub(crate) fn mount() -> RouterBuilder {
.exec()
.await?;
- Ok(DirectoryWithContents {
- directory,
- contents: file_paths
+ Ok(ExplorerData {
+ context: ExplorerContext::Location(location),
+ items: file_paths
.into_iter()
.map(|mut file_path| {
if let Some(file) = &mut file_path.file.as_mut().unwrap_or_else(
@@ -103,14 +121,12 @@ pub(crate) fn mount() -> RouterBuilder {
.config()
.data_directory()
.join(THUMBNAIL_CACHE_DIR_NAME)
- .join(location.id.to_string())
.join(&file.cas_id)
.with_extension("webp");
file.has_thumbnail = thumb_path.exists();
}
-
- file_path
+ ExplorerItem::Path(Box::new(file_path))
})
.collect(),
})
diff --git a/core/src/api/tags.rs b/core/src/api/tags.rs
index 81b0ee819..671941f5b 100644
--- a/core/src/api/tags.rs
+++ b/core/src/api/tags.rs
@@ -1,12 +1,15 @@
-use crate::{
- invalidate_query,
- prisma::{file, tag},
-};
-
-use rspc::Type;
+use rspc::{ErrorCode, Type};
use serde::Deserialize;
+use tracing::log::info;
use uuid::Uuid;
+use crate::{
+ api::locations::{ExplorerContext, ExplorerData, ExplorerItem},
+ encode::THUMBNAIL_CACHE_DIR_NAME,
+ invalidate_query,
+ prisma::{file, tag, tag_on_file},
+};
+
use super::{LibraryArgs, RouterBuilder};
#[derive(Type, Deserialize)]
@@ -15,10 +18,11 @@ pub struct TagCreateArgs {
pub color: String,
}
-#[derive(Type, Deserialize)]
+#[derive(Debug, Type, Deserialize)]
pub struct TagAssignArgs {
pub file_id: i32,
pub tag_id: i32,
+ pub unassign: bool,
}
#[derive(Type, Deserialize)]
@@ -30,12 +34,77 @@ pub struct TagUpdateArgs {
pub(crate) fn mount() -> RouterBuilder {
RouterBuilder::new()
- .query("get", |ctx, arg: LibraryArgs<()>| async move {
+ .query("getAll", |ctx, arg: LibraryArgs<()>| async move {
let (_, library) = arg.get_library(&ctx).await?;
Ok(library.db.tag().find_many(vec![]).exec().await?)
})
- .query("getFilesForTag", |ctx, arg: LibraryArgs| async move {
+ .query("getExplorerData", |ctx, arg: LibraryArgs| async move {
+ let (tag_id, library) = arg.get_library(&ctx).await?;
+
+ info!("Getting files for tag {}", tag_id);
+
+ let tag = library
+ .db
+ .tag()
+ .find_unique(tag::id::equals(tag_id))
+ .exec()
+ .await?
+ .ok_or_else(|| {
+ rspc::Error::new(ErrorCode::NotFound, format!("Tag not found"))
+ })?;
+
+ let files: Vec = library
+ .db
+ .file()
+ .find_many(vec![file::tags::some(vec![tag_on_file::tag_id::equals(
+ tag_id,
+ )])])
+ .with(file::paths::fetch(vec![]))
+ .exec()
+ .await?
+ .into_iter()
+ .map(|mut file| {
+ // sorry brendan
+ // grab the first path and tac on the name
+ let oldest_path = &file.paths.as_ref().unwrap()[0];
+ file.name = Some(oldest_path.name.clone());
+ file.extension = oldest_path.extension.clone();
+ // a long term fix for this would be to have the indexer give the Object a name and extension, sacrificing its own and only store newly found Path names that differ from the Object name
+
+ let thumb_path = library
+ .config()
+ .data_directory()
+ .join(THUMBNAIL_CACHE_DIR_NAME)
+ .join(&file.cas_id)
+ .with_extension("webp");
+
+ file.has_thumbnail = thumb_path.exists();
+
+ ExplorerItem::Object(Box::new(file))
+ })
+ .collect();
+
+ info!("Got files {}", files.len());
+
+ Ok(ExplorerData {
+ context: ExplorerContext::Tag(tag),
+ items: files,
+ })
+ })
+ .query("getForFile", |ctx, arg: LibraryArgs| async move {
+ let (file_id, library) = arg.get_library(&ctx).await?;
+
+ Ok(library
+ .db
+ .tag()
+ .find_many(vec![tag::tag_files::some(vec![
+ tag_on_file::file_id::equals(file_id),
+ ])])
+ .exec()
+ .await?)
+ })
+ .query("get", |ctx, arg: LibraryArgs| async move {
let (tag_id, library) = arg.get_library(&ctx).await?;
Ok(library
@@ -65,7 +134,7 @@ pub(crate) fn mount() -> RouterBuilder {
invalidate_query!(
library,
- "tags.get": LibraryArgs<()>,
+ "tags.getAll": LibraryArgs<()>,
LibraryArgs {
library_id: library.id,
arg: ()
@@ -80,10 +149,33 @@ pub(crate) fn mount() -> RouterBuilder {
|ctx, arg: LibraryArgs| async move {
let (args, library) = arg.get_library(&ctx).await?;
- library.db.tag_on_file().create(
- tag::id::equals(args.tag_id),
- file::id::equals(args.file_id),
- vec![],
+ if args.unassign {
+ library
+ .db
+ .tag_on_file()
+ .delete(tag_on_file::tag_id_file_id(args.tag_id, args.file_id))
+ .exec()
+ .await?;
+ } else {
+ library
+ .db
+ .tag_on_file()
+ .create(
+ tag::id::equals(args.tag_id),
+ file::id::equals(args.file_id),
+ vec![],
+ )
+ .exec()
+ .await?;
+ }
+
+ invalidate_query!(
+ library,
+ "tags.getForFile": LibraryArgs,
+ LibraryArgs {
+ library_id: library.id,
+ arg: args.file_id
+ }
);
Ok(())
@@ -106,7 +198,7 @@ pub(crate) fn mount() -> RouterBuilder {
invalidate_query!(
library,
- "tags.get": LibraryArgs<()>,
+ "tags.getAll": LibraryArgs<()>,
LibraryArgs {
library_id: library.id,
arg: ()
@@ -123,7 +215,7 @@ pub(crate) fn mount() -> RouterBuilder {
invalidate_query!(
library,
- "tags.get": LibraryArgs<()>,
+ "tags.getAll": LibraryArgs<()>,
LibraryArgs {
library_id: library.id,
arg: ()
diff --git a/core/src/encode/thumb.rs b/core/src/encode/thumb.rs
index 8dafcb0b4..e99906ddc 100644
--- a/core/src/encode/thumb.rs
+++ b/core/src/encode/thumb.rs
@@ -1,5 +1,5 @@
use crate::{
- api::{locations::GetExplorerDirArgs, CoreEvent, LibraryArgs},
+ api::{locations::LocationExplorerArgs, CoreEvent, LibraryArgs},
invalidate_query,
job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext},
library::LibraryContext,
@@ -56,8 +56,8 @@ impl StatefulJob for ThumbnailJob {
let thumbnail_dir = library_ctx
.config()
.data_directory()
- .join(THUMBNAIL_CACHE_DIR_NAME)
- .join(state.init.location_id.to_string());
+ .join(THUMBNAIL_CACHE_DIR_NAME);
+ // .join(state.init.location_id.to_string());
let location = library_ctx
.db
@@ -157,13 +157,14 @@ impl StatefulJob for ThumbnailJob {
let library_ctx = ctx.library_ctx();
invalidate_query!(
library_ctx,
- "locations.getExplorerDir": LibraryArgs,
+ "locations.getExplorerData": LibraryArgs,
LibraryArgs::new(
library_ctx.id,
- GetExplorerDirArgs {
+ LocationExplorerArgs {
location_id: state.init.location_id,
path: "".to_string(),
- limit: 100
+ limit: 100,
+ cursor: None,
}
)
);
diff --git a/core/src/file/cas/identifier.rs b/core/src/file/cas/identifier.rs
index b96ae7ad1..e193318a3 100644
--- a/core/src/file/cas/identifier.rs
+++ b/core/src/file/cas/identifier.rs
@@ -28,6 +28,7 @@ pub struct FileIdentifierJob {}
#[derive(Serialize, Deserialize, Clone)]
pub struct FileIdentifierJobInit {
pub location: location::Data,
+ pub sub_path: Option, // subpath to start from
}
#[derive(Serialize, Deserialize, Debug)]
@@ -46,6 +47,7 @@ impl From<&FilePathIdAndLocationIdCursor> for file_path::UniqueWhereParam {
pub struct FileIdentifierJobState {
total_count: usize,
task_count: usize,
+ location: location::Data,
location_path: PathBuf,
cursor: FilePathIdAndLocationIdCursor,
}
@@ -69,9 +71,15 @@ impl StatefulJob for FileIdentifierJob {
let library = ctx.library_ctx();
- let location_path = state
- .init
- .location
+ let location = library
+ .db
+ .location()
+ .find_unique(location::id::equals(state.init.location.id))
+ .exec()
+ .await?
+ .unwrap();
+
+ let location_path = location
.local_path
.as_ref()
.map(PathBuf::from)
@@ -89,6 +97,7 @@ impl StatefulJob for FileIdentifierJob {
state.data = Some(FileIdentifierJobState {
total_count,
task_count,
+ location,
location_path,
cursor: FilePathIdAndLocationIdCursor {
file_path_id: 1,
@@ -116,13 +125,14 @@ impl StatefulJob for FileIdentifierJob {
.expect("critical error: missing data on job state");
// get chunk of orphans to process
- let file_paths = match get_orphan_file_paths(&ctx.library_ctx(), &data.cursor).await {
- Ok(file_paths) => file_paths,
- Err(e) => {
- info!("Error getting orphan file paths: {:#?}", e);
- return Ok(());
- }
- };
+ let file_paths =
+ match get_orphan_file_paths(&ctx.library_ctx(), &data.cursor, data.location.id).await {
+ Ok(file_paths) => file_paths,
+ Err(e) => {
+ info!("Error getting orphan file paths: {:#?}", e);
+ return Ok(());
+ }
+ };
info!(
"Processing {:?} orphan files. ({} completed of {})",
file_paths.len(),
@@ -305,6 +315,7 @@ async fn count_orphan_file_paths(
async fn get_orphan_file_paths(
ctx: &LibraryContext,
cursor: &FilePathIdAndLocationIdCursor,
+ location_id: i32,
) -> Result, prisma_client_rust::QueryError> {
info!(
"discovering {} orphan file paths at cursor: {:?}",
@@ -315,6 +326,7 @@ async fn get_orphan_file_paths(
.find_many(vec![
file_path::file_id::equals(None),
file_path::is_dir::equals(false),
+ file_path::location_id::equals(location_id),
])
.order_by(file_path::id::order(Direction::Asc))
.cursor(cursor.into())
@@ -344,6 +356,8 @@ async fn prepare_file(
.as_ref()
.join(file_path.materialized_path.as_str());
+ info!("Reading path: {:?}", path);
+
let metadata = fs::metadata(&path).await?;
// let date_created: DateTime = metadata.created().unwrap().into();
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 99438e9eb..776a5e5f5 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -16,6 +16,7 @@ pub(crate) mod job;
pub(crate) mod library;
pub(crate) mod location;
pub(crate) mod node;
+pub(crate) mod object;
pub(crate) mod prisma;
pub(crate) mod util;
pub(crate) mod volume;
diff --git a/core/src/location/mod.rs b/core/src/location/mod.rs
index 418bac600..afc2981c8 100644
--- a/core/src/location/mod.rs
+++ b/core/src/location/mod.rs
@@ -263,6 +263,7 @@ pub async fn scan_location(
ctx.queue_job(Job::new(
FileIdentifierJobInit {
location: location.clone(),
+ sub_path: None,
},
Box::new(FileIdentifierJob {}),
))
diff --git a/core/src/object/mod.rs b/core/src/object/mod.rs
new file mode 100644
index 000000000..fd268ba87
--- /dev/null
+++ b/core/src/object/mod.rs
@@ -0,0 +1,67 @@
+// Objects are primarily created by the identifier from Paths
+// Some Objects are purely virtual, unless they have one or more associated Paths, which refer to a file found in a Location
+// Objects are what can be added to Spaces
+
+use rspc::Type;
+use serde::{Deserialize, Serialize};
+
+use crate::prisma;
+
+// The response to provide the Explorer when looking at Objects
+#[derive(Debug, Serialize, Deserialize, Type)]
+pub struct ObjectsForExplorer {
+ pub objects: Vec,
+ // pub context: ExplorerContext,
+}
+
+// #[derive(Debug, Serialize, Deserialize, Type)]
+// pub enum ExplorerContext {
+// Location(Box),
+// Space(Box),
+// Tag(Box),
+// // Search(Box),
+// }
+
+#[derive(Debug, Serialize, Deserialize, Type)]
+pub enum ObjectData {
+ Object(Box),
+ Path(Box),
+}
+
+#[derive(Debug, Serialize, Deserialize, Type)]
+pub enum ObjectKind {
+ // A file that can not be identified by the indexer
+ Unknown,
+ // A known filetype, but without specific support
+ Document,
+ // A virtual filesystem directory
+ Folder,
+ // A file that contains human-readable text
+ TextFile,
+ // A virtual directory int
+ Package,
+ // An image file
+ Image,
+ // An audio file
+ Audio,
+ // A video file
+ Video,
+ // A compressed archive of data
+ Archive,
+ // An executable, program or application
+ Executable,
+ // A link to another object
+ Alias,
+ // Raw bytes encrypted by Spacedrive with self contained metadata
+ EncryptedBytes,
+ // A link can open web pages, apps or Spaces
+ Link,
+ // A special filetype that represents a preserved webpage
+ WebPageArchive,
+ // A widget is a mini app that can be placed in a Space at various sizes, associated Widget struct required
+ Widget,
+ // Albums can only have one level of children, and are associated with the Album struct
+ Album,
+ // Its like a folder, but appears like a stack of files, designed for burst photos / associated groups of files
+ Collection,
+}
diff --git a/core/src/util/db.rs b/core/src/util/db.rs
index 35ba43999..08a59a021 100644
--- a/core/src/util/db.rs
+++ b/core/src/util/db.rs
@@ -101,9 +101,27 @@ pub async fn load_and_migrate(db_url: &str) -> Result>();
- let steps = &steps[0..steps.len() - 1];
+ let step_count = steps.len();
+ let steps = &steps[0..step_count - 1];
+
for (i, step) in steps.iter().enumerate() {
- client._execute_raw(raw!(*step)).exec().await?;
+ match client._execute_raw(raw!(*step)).exec().await {
+ Ok(_) => {}
+ Err(e) => {
+ // remove the failed migration record so next time it will be retried
+ // potentially an issue if steps were already applied, look into generating down migrations
+ client
+ .migration()
+ .delete(migration::checksum::equals(checksum.clone()))
+ .exec()
+ .await?;
+
+ // TODO: Show UI alert with error message
+ panic!("Error applying migration step: {}", e);
+ }
+ }
+ // Note: there isn't much point storing the steps in the db if we don't generate down migrations and write logic to run the already applied steps in reverse for a failed migration.
+ // for now if a migration fails we abort entirely (see above panic)
client
.migration()
.update(
diff --git a/docs/architecture/albums.md b/docs/architecture/albums.md
new file mode 100644
index 000000000..0b024bcdf
--- /dev/null
+++ b/docs/architecture/albums.md
@@ -0,0 +1,3 @@
+# Albums
+
+you can put photos here
\ No newline at end of file
diff --git a/docs/architecture/database.md b/docs/architecture/database.md
index 7c905126b..9fc5d324f 100644
--- a/docs/architecture/database.md
+++ b/docs/architecture/database.md
@@ -1,9 +1,3 @@
-## Database backup
+# Database
-## Database migrations
-
-Currently, migrations are applied on app launch with no visual feedback, backup or error handling.
-
-It doesn't appear that migrations are applied successfully
-
-##
+prisma client rust, sqlite, migrations, backup
\ No newline at end of file
diff --git a/docs/architecture/explorer.md b/docs/architecture/explorer.md
new file mode 100644
index 000000000..ab04aafbc
--- /dev/null
+++ b/docs/architecture/explorer.md
@@ -0,0 +1,3 @@
+# Explorer
+
+using the interface, features
\ No newline at end of file
diff --git a/docs/architecture/extensions.md b/docs/architecture/extensions.md
index e69de29bb..ed191fd47 100644
--- a/docs/architecture/extensions.md
+++ b/docs/architecture/extensions.md
@@ -0,0 +1,3 @@
+# Extensions
+
+extended functionality of Spacedrive
\ No newline at end of file
diff --git a/docs/architecture/filesystem.md b/docs/architecture/filesystem.md
new file mode 100644
index 000000000..863b616ca
--- /dev/null
+++ b/docs/architecture/filesystem.md
@@ -0,0 +1,6 @@
+# sVFS???
+
+sVFS is a decentralized virtual filesystem
+
+*not sure what to call this yet*
+
diff --git a/docs/architecture/jobs.md b/docs/architecture/jobs.md
index e69de29bb..33f69f873 100644
--- a/docs/architecture/jobs.md
+++ b/docs/architecture/jobs.md
@@ -0,0 +1,3 @@
+# Jobs
+
+jobs are computation tasks performed by nodes in a Spacedrive network, they can be created by any node and performed by any or all nodes.
\ No newline at end of file
diff --git a/docs/architecture/libraries.md b/docs/architecture/libraries.md
new file mode 100644
index 000000000..4da0363c8
--- /dev/null
+++ b/docs/architecture/libraries.md
@@ -0,0 +1,4 @@
+# Libraries
+
+A library is a database, you can have many of them. They contain all the Spacedrive data, inluding file structure and metadata.
+
diff --git a/docs/architecture/locations.md b/docs/architecture/locations.md
new file mode 100644
index 000000000..d7a496026
--- /dev/null
+++ b/docs/architecture/locations.md
@@ -0,0 +1,3 @@
+# Locations
+
+indexing, identifying, watching, .spacedrive folder, online/offline
\ No newline at end of file
diff --git a/docs/architecture/nodes.md b/docs/architecture/nodes.md
new file mode 100644
index 000000000..33524444f
--- /dev/null
+++ b/docs/architecture/nodes.md
@@ -0,0 +1,3 @@
+# Nodes
+
+p2p, connecting nodes, protocols.
\ No newline at end of file
diff --git a/docs/architecture/objects.md b/docs/architecture/objects.md
new file mode 100644
index 000000000..e3420572f
--- /dev/null
+++ b/docs/architecture/objects.md
@@ -0,0 +1,32 @@
+# Objects
+
+Objects are primarily created by the identifier from Paths. They can be created from any kind of file or directory. All metadata created around files in Spacedrive are directly attached to the Object for that file.
+
+A CAS id is generated from samples of the byte data, which is used to associate Objects uniquely with logical Paths found in a location.
+
+Some Objects are purely virtual, meaning they have no Path and are likely only used in a Space.
+
+
+
+## Types of object
+
+| Name | Description | Code |
+| ---------------- | ------------------------------------------------------------ | ---- |
+| Unknown | A file that can not be identified by the indexer | 0 |
+| Document | A known filetype, but without specific support | 1 |
+| Folder | A virtual filesystem directory | 2 |
+| Text File | A file that contains human-readable text | 3 |
+| Package | A folder that opens an application | 4 |
+| Image | An image file | 5 |
+| Audio | An audio file | 6 |
+| Video | A video file | 7 |
+| Archive | A compressed archive of data | 8 |
+| Executable | An executable program or application | 9 |
+| Alias | A link to another Object | 10 |
+| Encrypted Bytes | Raw bytes with self contained metadata | 11 |
+| Link | A link to a web page, application or Space | 12 |
+| Web Page Archive | A snapshot of a webpage, with HTML, JS, images and screenshot | 13 |
+| Widget | A widget is a mini app that can be placed in a Space at various sizes, associated Widget struct required | 14 |
+| Album | Albums can only have one level of children, and are associated with the Album struct | 15 |
+| Collection | Its like a folder, but appears like a stack of files, designed for burst photos/associated groups of files | 16 |
+
diff --git a/docs/architecture/preview-media.md b/docs/architecture/preview-media.md
new file mode 100644
index 000000000..271f663a1
--- /dev/null
+++ b/docs/architecture/preview-media.md
@@ -0,0 +1,7 @@
+# Preview Media
+
+Spacedrive generates compressed preview media for images, videos and text files.
+
+
+
+ffmpeg, syncing, security
\ No newline at end of file
diff --git a/docs/architecture/search.md b/docs/architecture/search.md
new file mode 100644
index 000000000..8a0071c40
--- /dev/null
+++ b/docs/architecture/search.md
@@ -0,0 +1,9 @@
+# Search
+
+Press CTRL+F while on a Spacedrive window to access search.
+
+By default you will search the active library, however by checking "Search all Libraries" you can perform a simotanious search of all libraries loaded on a Node.
+
+Search results return Objects, Locations, Albums, Tags and Spaces
+
+Search can be filtered by `ObjectKind`, as well as dates.
\ No newline at end of file
diff --git a/docs/architecture/spaces.md b/docs/architecture/spaces.md
new file mode 100644
index 000000000..b2ae5a0e4
--- /dev/null
+++ b/docs/architecture/spaces.md
@@ -0,0 +1,9 @@
+# Spaces
+
+Spaces are virtual folders that can be shared publically on the internet, or privately with friends, family and teams. Spaces contain [Objects]() which can be physically stored on any connected Node, or by Spacedrive as a service. Objects can be organised and presented spacially, with various layouts and variable grid placements. Color theme, icon packs and typeogrpahy can be customized per Space.
+
+
+
+Objects can be added to a Space manually, or by matching a defined ruleset, similar to Tags.
+
+Spacedrive comes with pre-defined Spaces, such as: photos, videos, screenshots, documents, .
\ No newline at end of file
diff --git a/docs/architecture/sync.md b/docs/architecture/sync.md
new file mode 100644
index 000000000..0b3f809af
--- /dev/null
+++ b/docs/architecture/sync.md
@@ -0,0 +1,14 @@
+# Sync
+
+Spacedrive synchronizes library data in realtime across the distributed network of Nodes.
+
+Using a Unique Hybrid Logicial Clock for distributed time synchronization.
+
+A combination of several property level CRDT types:
+
+- **Local data** - migrations, statistics, sync events
+- **Owned data** - locations, paths, volumes
+- **Shared data** - objects, tags, spaces, jobs
+- **Relationship data** - many to many tables
+
+Built in Rust on top of Prisma, it uses the schema file to determine these sync rules.
\ No newline at end of file
diff --git a/docs/architecture/tags.md b/docs/architecture/tags.md
new file mode 100644
index 000000000..f699b71ae
--- /dev/null
+++ b/docs/architecture/tags.md
@@ -0,0 +1,5 @@
+# Tags
+
+Spacedrive heavily incentivize using tags to organize files efficiently by giving them more functionality and easier ways to apply them, individually and in bulk.
+
+ways to add tags, tag automation
\ No newline at end of file
diff --git a/docs/changelog/10-4-22_0.1.0.md b/docs/changelog/10-4-22_0.1.0.md
deleted file mode 100644
index cf8c40042..000000000
--- a/docs/changelog/10-4-22_0.1.0.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# `0.1.0`
-
-**Sunday, April 10th 2022**
-
-A change happened.
-
-- A very specific change.
-- Another extremely specific change.
-- I love change.
diff --git a/docs/changelog/index.md b/docs/changelog/index.md
index f011ee97b..db2a3989b 100644
--- a/docs/changelog/index.md
+++ b/docs/changelog/index.md
@@ -1,3 +1,68 @@
-# Changelog
+--------------------------------COMING SOON--------------------------------
-No releases yet.
+# 0.1.0_beta
+
+After __ months of development we are extremely excited to be releasing the first version of Spacedrive as an early public beta.
+
+This is an MVP, and by no means feature complete. Please test out the features listed below and give us feedback via Discord, email or GitHub Issues :D
+
+This release is missing database synchronization between nodes (your devices), for now this renders connecting nodes useless, other than to transfer individual files. But don't worry, its coming very soon!
+
+*Features:*
+
+- Support for Windows, Linux and macOS, iOS and Android.
+
+- Basic onboarding flow, determine use-case and preferences.
+
+- Create [Libraries](../architecture/libraries.md) and switch between them.
+
+- Connect multiple [Nodes](../architecture/nodes.md) to a Library via LAN.
+
+- Add [Locations](../architecture/locations.md) to import files into Spacedrive.
+ - Indexer watch for changes and performs light re-scans.
+
+ - Identifier generates checksum and categorizes files into [Objects]()
+
+ - Define rules for indexer to ignore certain files or folders.
+
+ *Eventually Clouds will be supported and added as Cloud Locations*
+
+- Browse Locations via the [Explorer](../architecture/explorer.md) and view previews and metadata.
+ - Viewer options: row/grid item size, gap adjustment, show/hide info.
+ - Context menu: rename, copy, duplicate, delete, favorite and add tags.
+ - Multi-select with dedicated context menu options.
+ - Open with default OS app, in-app viewer (images/text only) or Apple Quicklook
+
+- Automatically identify unique files to discover duplicates, shown in the inspector.
+
+- Generate [Preview Media](../architecture/preview-media.md) for image, video and text.
+
+- Create [Tags](../architecture/tags.md) and assign them to files, browse Tags in the Explorer.
+
+- Create [Spaces](../architecture/spaces.md) to organize and present files.
+
+ - Automated Spaces can include files that match criteria.
+
+ *Eventually Spaces will be sharable, publically or privately*
+
+- Create photo [Albums](../architecture/albums.md) and add images.
+
+- Library statistics: total capacity, database size, preview media size, free space.
+
+- [Search](../architecture/search.md) Library via search bar or CTRL+F.
+
+ - Searches online and offline Locations, Spaces, Tags and Albums.
+
+- Drag and drop file transfer on a keybind.
+
+ - Defaults to CTRL+Space, also possible from Explorer context menu.
+
+- Customize sidebar freely with section headings and flexible slots, include default layout.
+
+- Pause and resume [Jobs](../architecture/jobs.md) with recovery on crash via Job Manager widget.
+
+- Multi-window support.
+
+- Update installer.
+
+- Optional crash reporting.
\ No newline at end of file
diff --git a/docs/product/ideas.md b/docs/product/ideas.md
deleted file mode 100644
index ada925939..000000000
--- a/docs/product/ideas.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# Core view
-
-The primary screen in the Spacedrive app is of your entire virtual network, every "core" is a device under your full command. It is presented in a large panel list view, featuring bold visuals and quick interactions with your fleet.
-
-Recent files, recent locations and settings are part of these panels, that can be customized at will.
-Visual indicators report the live status of this device.
-
-Key statistics of this devices are also present on these panels.
-
-A "Core" represents a device in your "Space".
diff --git a/packages/client/package.json b/packages/client/package.json
index fbdb41eed..1c9df3845 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -35,6 +35,6 @@
"typescript": "^4.7.4"
},
"peerDependencies": {
- "react": "^18.0.0"
+ "react": "^18.2.0"
}
}
diff --git a/packages/client/src/stores/useExplorerStore.ts b/packages/client/src/stores/useExplorerStore.ts
index c16399e34..5cb66ac2f 100644
--- a/packages/client/src/stores/useExplorerStore.ts
+++ b/packages/client/src/stores/useExplorerStore.ts
@@ -1,31 +1,53 @@
+import produce from 'immer';
import create from 'zustand';
type LayoutMode = 'list' | 'grid';
+export enum ExplorerKind {
+ Location,
+ Tag,
+ Space
+}
+
type ExplorerStore = {
- selectedRowIndex: number;
layoutMode: LayoutMode;
- setSelectedRowIndex: (index: number) => void;
- locationId: number;
- setLocationId: (index: number) => void;
+ locationId: number | null; // used by top bar
+ showInspector: boolean;
+ selectedRowIndex: number;
+ multiSelectIndexes: number[];
+ contextMenuObjectId: number | null;
newThumbnails: Record;
addNewThumbnail: (cas_id: string) => void;
- setLayoutMode: (mode: LayoutMode) => void;
+ selectMore: (indexes: number[]) => void;
reset: () => void;
+ set: (changes: Partial) => void;
};
export const useExplorerStore = create((set) => ({
layoutMode: 'grid',
+ locationId: null,
+ showInspector: true,
selectedRowIndex: 1,
- setSelectedRowIndex: (index) => set((state) => ({ ...state, selectedRowIndex: index })),
- locationId: -1,
- setLocationId: (id: number) => set((state) => ({ ...state, locationId: id })),
+ multiSelectIndexes: [],
+ contextMenuObjectId: -1,
newThumbnails: {},
- addNewThumbnail: (cas_id: string) =>
- set((state) => ({
- ...state,
- newThumbnails: { ...state.newThumbnails, [cas_id]: true }
- })),
- setLayoutMode: (mode: LayoutMode) => set((state) => ({ ...state, layoutMode: mode })),
- reset: () => set(() => ({}))
+ addNewThumbnail: (cas_id) =>
+ set((state) =>
+ produce(state, (draft) => {
+ draft.newThumbnails[cas_id] = true;
+ })
+ ),
+ selectMore: (indexes) => {
+ set((state) =>
+ produce(state, (draft) => {
+ if (!draft.multiSelectIndexes.length && indexes.length) {
+ draft.multiSelectIndexes = [draft.selectedRowIndex, ...indexes];
+ } else {
+ draft.multiSelectIndexes = [...new Set([...draft.multiSelectIndexes, ...indexes])];
+ }
+ })
+ );
+ },
+ reset: () => set(() => ({})),
+ set: (changes) => set((state) => ({ ...state, ...changes }))
}));
diff --git a/packages/client/src/types/file.ts b/packages/client/src/types/file.ts
new file mode 100644
index 000000000..e48d01533
--- /dev/null
+++ b/packages/client/src/types/file.ts
@@ -0,0 +1,16 @@
+import { FilePath } from '@sd/core';
+
+export interface ExplorerItem {
+ id: number;
+ name: string;
+ is_dir: boolean;
+ // kind: ObjectKind;
+ extension: string;
+ size_in_bytes: number;
+ created_at: string;
+ updated_at: string;
+ favorite?: boolean;
+
+ // computed
+ paths?: FilePath[];
+}
diff --git a/packages/client/src/types/index.ts b/packages/client/src/types/index.ts
new file mode 100644
index 000000000..706b0d228
--- /dev/null
+++ b/packages/client/src/types/index.ts
@@ -0,0 +1 @@
+export * from './file';
diff --git a/packages/config/base.tsconfig.json b/packages/config/base.tsconfig.json
index ee70cd9f1..459363828 100644
--- a/packages/config/base.tsconfig.json
+++ b/packages/config/base.tsconfig.json
@@ -19,7 +19,7 @@
"strict": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
- "jsx": "react",
+ "jsx": "react-jsx",
"paths": {
"@sd/interface": ["../../packages/interface"],
"@sd/ui": ["../../packages/ui"],
diff --git a/packages/interface/package.json b/packages/interface/package.json
index 1a1448813..2ea606988 100644
--- a/packages/interface/package.json
+++ b/packages/interface/package.json
@@ -18,7 +18,7 @@
"@apollo/client": "^3.6.9",
"@fontsource/inter": "^4.5.11",
"@headlessui/react": "^1.6.6",
- "@heroicons/react": "^1.0.6",
+ "@heroicons/react": "^2.0.10",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dropdown-menu": "^1.0.0",
"@radix-ui/react-icons": "^1.1.1",
@@ -29,6 +29,7 @@
"@sd/client": "workspace:*",
"@sd/core": "workspace:*",
"@sd/ui": "workspace:*",
+ "@tailwindcss/forms": "^0.5.2",
"@tanstack/react-query": "^4.0.10",
"@tanstack/react-query-devtools": "^4.0.10",
"@types/styled-components": "^5.1.25",
@@ -66,6 +67,7 @@
"tailwindcss": "^3.1.6",
"use-count-up": "^3.0.1",
"use-debounce": "^8.0.3",
+ "zod": "^3.18.0",
"zustand": "4.0.0"
},
"devDependencies": {
diff --git a/packages/interface/src/AppLayout.tsx b/packages/interface/src/AppLayout.tsx
index 0c3ecbb11..a2670fbfe 100644
--- a/packages/interface/src/AppLayout.tsx
+++ b/packages/interface/src/AppLayout.tsx
@@ -3,7 +3,7 @@ import clsx from 'clsx';
import React, { useContext } from 'react';
import { Outlet } from 'react-router-dom';
-import { Sidebar } from './components/file/Sidebar';
+import { Sidebar } from './components/layout/Sidebar';
export function AppLayout() {
const appProps = useContext(AppPropsContext);
diff --git a/packages/interface/src/AppRouter.tsx b/packages/interface/src/AppRouter.tsx
index 61422b527..df99204ea 100644
--- a/packages/interface/src/AppRouter.tsx
+++ b/packages/interface/src/AppRouter.tsx
@@ -6,11 +6,11 @@ import { AppLayout } from './AppLayout';
import { NotFound } from './NotFound';
import { ContentScreen } from './screens/Content';
import { DebugScreen } from './screens/Debug';
-import { ExplorerScreen } from './screens/Explorer';
+import { LocationExplorer } from './screens/LocationExplorer';
import { OverviewScreen } from './screens/Overview';
import { PhotosScreen } from './screens/Photos';
import { RedirectPage } from './screens/Redirect';
-import { TagScreen } from './screens/Tag';
+import { TagExplorer } from './screens/TagExplorer';
import { SettingsScreen } from './screens/settings/Settings';
import AppearanceSettings from './screens/settings/client/AppearanceSettings';
import ExtensionSettings from './screens/settings/client/ExtensionsSettings';
@@ -101,8 +101,8 @@ export function AppRouter() {
} />
} />
- } />
- } />
+ } />
+ } />
} />
diff --git a/packages/interface/src/components/device/Device.tsx b/packages/interface/src/components/device/Device.tsx
index 3b5c98305..5f1f9a462 100644
--- a/packages/interface/src/components/device/Device.tsx
+++ b/packages/interface/src/components/device/Device.tsx
@@ -1,10 +1,10 @@
-import { KeyIcon } from '@heroicons/react/outline';
-import { CogIcon, LockClosedIcon } from '@heroicons/react/solid';
+import { KeyIcon } from '@heroicons/react/24/outline';
+import { CogIcon, LockClosedIcon } from '@heroicons/react/24/solid';
import { Button } from '@sd/ui';
import { Cloud, Desktop, DeviceMobileCamera, DotsSixVertical, Laptop } from 'phosphor-react';
import React, { useState } from 'react';
-import FileItem from '../file/FileItem';
+import FileItem from '../explorer/FileItem';
import Loader from '../primitive/Loader';
import ProgressBar from '../primitive/ProgressBar';
import { Tooltip } from '../tooltip/Tooltip';
@@ -56,29 +56,19 @@ export function Device(props: DeviceProps) {
)}
-
-
-
+
-
-
+
- {props.locations.map((location, key) => (
-
handleSelect(location.name)}
- />
- ))}
{props.locations.length === 0 && (
No locations
)}
diff --git a/packages/interface/src/components/dialog/CreateLibraryDialog.tsx b/packages/interface/src/components/dialog/CreateLibraryDialog.tsx
new file mode 100644
index 000000000..ef250038d
--- /dev/null
+++ b/packages/interface/src/components/dialog/CreateLibraryDialog.tsx
@@ -0,0 +1,44 @@
+import { useBridgeMutation } from '@sd/client';
+import { Input } from '@sd/ui';
+import React, { useState } from 'react';
+
+import Dialog from '../layout/Dialog';
+
+interface Props {
+ children: React.ReactNode;
+}
+
+export default function CreateLibraryDialog(props: Props) {
+ const [openCreateModal, setOpenCreateModal] = useState(false);
+ const [newLibName, setNewLibName] = useState('');
+
+ const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation(
+ 'library.create',
+ {
+ onSuccess: () => {
+ setOpenCreateModal(false);
+ }
+ }
+ );
+
+ return (
+
+ );
+}
diff --git a/packages/interface/src/components/dialog/DeleteLibraryDialog.tsx b/packages/interface/src/components/dialog/DeleteLibraryDialog.tsx
new file mode 100644
index 000000000..a11c2fc1c
--- /dev/null
+++ b/packages/interface/src/components/dialog/DeleteLibraryDialog.tsx
@@ -0,0 +1,37 @@
+import { useBridgeMutation } from '@sd/client';
+import { LibraryConfigWrapped } from '@sd/core';
+import { Input } from '@sd/ui';
+import React, { useState } from 'react';
+
+import Dialog from '../layout/Dialog';
+
+interface Props {
+ children: React.ReactNode;
+ libraryUuid: string;
+}
+
+export default function DeleteLibraryDialog(props: Props) {
+ const [openDeleteModal, setOpenDeleteModal] = useState(false);
+
+ const { mutate: deleteLib, isLoading: libDeletePending } = useBridgeMutation('library.delete', {
+ onSuccess: () => {
+ setOpenDeleteModal(false);
+ }
+ });
+
+ return (
+
+
set({ showInspector: !showInspector })}
+ className="my-2"
+ icon={SidebarSimple}
+ />
= (props) => {
{
name: 'Generate Thumbs',
icon: ArrowsClockwise,
- onPress: () => generateThumbsForLocation({ id: locationId, path: '' })
+ onPress: () =>
+ locationId && generateThumbsForLocation({ id: locationId, path: '' })
},
{
name: 'Identify Unique',
icon: ArrowsClockwise,
- onPress: () => identifyUniqueFiles({ id: locationId, path: '' })
+ onPress: () => locationId && identifyUniqueFiles({ id: locationId, path: '' })
}
]
]}
diff --git a/packages/interface/src/components/location/LocationListItem.tsx b/packages/interface/src/components/location/LocationListItem.tsx
index ebb732bc0..f4a677041 100644
--- a/packages/interface/src/components/location/LocationListItem.tsx
+++ b/packages/interface/src/components/location/LocationListItem.tsx
@@ -1,9 +1,9 @@
-import { RefreshIcon } from '@heroicons/react/outline';
-import { TrashIcon } from '@heroicons/react/solid';
+import { TrashIcon } from '@heroicons/react/24/solid';
import { useLibraryMutation } from '@sd/client';
import { Location } from '@sd/core';
import { Button } from '@sd/ui';
import clsx from 'clsx';
+import { Repeat } from 'phosphor-react';
import React, { useState } from 'react';
import { Folder } from '../icons/Folder';
@@ -77,7 +77,7 @@ export default function LocationListItem({ location }: LocationListItemProps) {
locRescan(location.id);
}}
>
-
+
{/*
-
-