mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-02-19 23:25:51 -05:00
Enhance file querying and alternate instances handling
- Updated `FileByIdQuery` and `FileByPathQuery` to populate alternate paths for files with the same content ID, improving data retrieval for file instances. - Introduced `get_alternate_paths` method in both queries to fetch alternate file paths from the database. - Modified the `InstancesTab` component to utilize a new query for alternate instances, enhancing the user interface with detailed instance information. - Updated TypeScript types to support the new alternate instances query structure, ensuring type safety across the application. - Adjusted various components to improve the display of alternate file instances, including device and path information.
This commit is contained in:
360
core/src/ops/files/query/alternate_instances.rs
Normal file
360
core/src/ops/files/query/alternate_instances.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
//! Query to get all alternate instances of a file by entry ID
|
||||
//!
|
||||
//! This query finds all other entries that share the same content_id and returns
|
||||
//! them as complete File objects with all related data (tags, sidecars, media data).
|
||||
|
||||
use crate::infra::query::{QueryError, QueryResult};
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
domain::{addressing::SdPath, content_identity::ContentIdentity, file::File},
|
||||
infra::db::entities::{
|
||||
content_identity, device, directory_paths, entry, location, sidecar, tag, user_metadata,
|
||||
user_metadata_tag, video_media_data,
|
||||
},
|
||||
infra::query::LibraryQuery,
|
||||
};
|
||||
use sea_orm::{ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Input for alternate instances query
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct AlternateInstancesInput {
|
||||
/// The entry UUID to find alternates for
|
||||
pub entry_uuid: Uuid,
|
||||
}
|
||||
|
||||
/// Output containing alternate instances
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct AlternateInstancesOutput {
|
||||
/// All instances of this file (including the original)
|
||||
pub instances: Vec<File>,
|
||||
/// Total number of instances found
|
||||
pub total_count: u32,
|
||||
}
|
||||
|
||||
/// Query to get alternate instances of a file
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct AlternateInstancesQuery {
|
||||
pub input: AlternateInstancesInput,
|
||||
}
|
||||
|
||||
impl AlternateInstancesQuery {
|
||||
pub fn new(entry_uuid: Uuid) -> Self {
|
||||
Self {
|
||||
input: AlternateInstancesInput { entry_uuid },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LibraryQuery for AlternateInstancesQuery {
|
||||
type Input = AlternateInstancesInput;
|
||||
type Output = AlternateInstancesOutput;
|
||||
|
||||
fn from_input(input: Self::Input) -> QueryResult<Self> {
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
context: Arc<CoreContext>,
|
||||
session: crate::infra::api::SessionContext,
|
||||
) -> QueryResult<Self::Output> {
|
||||
let library_id = session
|
||||
.current_library_id
|
||||
.ok_or_else(|| QueryError::Internal("No library in session".to_string()))?;
|
||||
|
||||
let library = context
|
||||
.libraries()
|
||||
.await
|
||||
.get_library(library_id)
|
||||
.await
|
||||
.ok_or_else(|| QueryError::Internal("Library not found".to_string()))?;
|
||||
|
||||
let db = library.db();
|
||||
|
||||
// Find the original entry
|
||||
let original_entry = entry::Entity::find()
|
||||
.filter(entry::Column::Uuid.eq(self.input.entry_uuid))
|
||||
.one(db.conn())
|
||||
.await?
|
||||
.ok_or_else(|| QueryError::Internal("Entry not found".to_string()))?;
|
||||
|
||||
// Get the content_id
|
||||
let content_id = original_entry.content_id.ok_or_else(|| {
|
||||
QueryError::Internal(
|
||||
"Entry has no content identity, cannot find alternates".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Find all entries with the same content_id
|
||||
let alternate_entries = entry::Entity::find()
|
||||
.filter(entry::Column::ContentId.eq(content_id))
|
||||
.all(db.conn())
|
||||
.await?;
|
||||
|
||||
if alternate_entries.is_empty() {
|
||||
return Ok(AlternateInstancesOutput {
|
||||
instances: Vec::new(),
|
||||
total_count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Batch load content identity
|
||||
let content_identity_model = content_identity::Entity::find_by_id(content_id)
|
||||
.one(db.conn())
|
||||
.await?
|
||||
.ok_or_else(|| QueryError::Internal("Content identity not found".to_string()))?;
|
||||
|
||||
let content_uuid = content_identity_model.uuid;
|
||||
|
||||
// Batch load sidecars
|
||||
let sidecars = if let Some(ci_uuid) = content_uuid {
|
||||
sidecar::Entity::find()
|
||||
.filter(sidecar::Column::ContentUuid.eq(ci_uuid))
|
||||
.all(db.conn())
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|s| crate::domain::file::Sidecar {
|
||||
id: s.id,
|
||||
content_uuid: s.content_uuid,
|
||||
kind: s.kind,
|
||||
variant: s.variant,
|
||||
format: s.format,
|
||||
status: s.status,
|
||||
size: s.size,
|
||||
created_at: s.created_at,
|
||||
updated_at: s.updated_at,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Batch load tags for all entries
|
||||
let entry_uuids: Vec<Uuid> = alternate_entries.iter().filter_map(|e| e.uuid).collect();
|
||||
|
||||
let mut tags_by_entry: HashMap<Uuid, Vec<crate::domain::tag::Tag>> = HashMap::new();
|
||||
|
||||
if !entry_uuids.is_empty() || content_uuid.is_some() {
|
||||
// Load user_metadata for entries and content
|
||||
let mut filter = user_metadata::Column::EntryUuid.is_in(entry_uuids.clone());
|
||||
if let Some(ci_uuid) = content_uuid {
|
||||
filter = filter.or(user_metadata::Column::ContentIdentityUuid.eq(ci_uuid));
|
||||
}
|
||||
|
||||
let metadata_records = user_metadata::Entity::find()
|
||||
.filter(filter)
|
||||
.all(db.conn())
|
||||
.await?;
|
||||
|
||||
if !metadata_records.is_empty() {
|
||||
let metadata_ids: Vec<i32> = metadata_records.iter().map(|m| m.id).collect();
|
||||
|
||||
// Load user_metadata_tag records
|
||||
let metadata_tags = user_metadata_tag::Entity::find()
|
||||
.filter(user_metadata_tag::Column::UserMetadataId.is_in(metadata_ids))
|
||||
.all(db.conn())
|
||||
.await?;
|
||||
|
||||
if !metadata_tags.is_empty() {
|
||||
let tag_ids: Vec<i32> = metadata_tags.iter().map(|mt| mt.tag_id).collect();
|
||||
|
||||
// Load tag entities
|
||||
let tag_models = tag::Entity::find()
|
||||
.filter(tag::Column::Id.is_in(tag_ids))
|
||||
.all(db.conn())
|
||||
.await?;
|
||||
|
||||
// Build tag_id -> Tag mapping
|
||||
let tag_map: HashMap<i32, crate::domain::tag::Tag> = tag_models
|
||||
.into_iter()
|
||||
.filter_map(|t| {
|
||||
let db_id = t.id;
|
||||
crate::ops::tags::manager::model_to_domain(t)
|
||||
.ok()
|
||||
.map(|tag| (db_id, tag))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Build metadata_id -> Vec<Tag> mapping
|
||||
let mut tags_by_metadata: HashMap<i32, Vec<crate::domain::tag::Tag>> =
|
||||
HashMap::new();
|
||||
for mt in metadata_tags {
|
||||
if let Some(tag) = tag_map.get(&mt.tag_id) {
|
||||
tags_by_metadata
|
||||
.entry(mt.user_metadata_id)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(tag.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Map tags to entries (prioritize entry-scoped, fall back to content-scoped)
|
||||
for metadata in &metadata_records {
|
||||
if let Some(tags) = tags_by_metadata.get(&metadata.id) {
|
||||
// Entry-scoped metadata (higher priority)
|
||||
if let Some(entry_uuid) = metadata.entry_uuid {
|
||||
tags_by_entry.insert(entry_uuid, tags.clone());
|
||||
}
|
||||
// Content-scoped metadata (applies to all entries with this content)
|
||||
else if let Some(_content_uuid) = metadata.content_identity_uuid {
|
||||
// Apply to all entries
|
||||
for entry_uuid in &entry_uuids {
|
||||
tags_by_entry
|
||||
.entry(*entry_uuid)
|
||||
.or_insert_with(|| tags.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build content identity domain object
|
||||
let content_identity_domain = ContentIdentity {
|
||||
uuid: content_uuid.unwrap_or_else(Uuid::new_v4),
|
||||
kind: crate::domain::ContentKind::from_id(content_identity_model.kind_id),
|
||||
content_hash: content_identity_model.content_hash,
|
||||
integrity_hash: content_identity_model.integrity_hash,
|
||||
mime_type_id: content_identity_model.mime_type_id,
|
||||
text_content: content_identity_model.text_content,
|
||||
total_size: content_identity_model.total_size,
|
||||
entry_count: content_identity_model.entry_count,
|
||||
first_seen_at: content_identity_model.first_seen_at,
|
||||
last_verified_at: content_identity_model.last_verified_at,
|
||||
};
|
||||
|
||||
// Load media data if available
|
||||
let video_media_data = if let Some(video_id) = content_identity_model.video_media_data_id {
|
||||
video_media_data::Entity::find_by_id(video_id)
|
||||
.one(db.conn())
|
||||
.await?
|
||||
.map(Into::into)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Convert each entry to a complete File object
|
||||
let mut instances = Vec::new();
|
||||
for entry_model in alternate_entries {
|
||||
// Resolve full path for this entry
|
||||
let sd_path = match self.resolve_entry_path(&entry_model, db.conn()).await {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to resolve path for entry {}: {}", entry_model.id, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Create File from entry model
|
||||
let mut file = File::from_entity_model(entry_model.clone(), sd_path);
|
||||
|
||||
// Add content identity, sidecars, and media data
|
||||
file.content_identity = Some(content_identity_domain.clone());
|
||||
file.sidecars = sidecars.clone();
|
||||
file.video_media_data = video_media_data.clone();
|
||||
file.content_kind = content_identity_domain.kind;
|
||||
file.duration_seconds = video_media_data.as_ref().and_then(|v| v.duration_seconds);
|
||||
|
||||
// Add tags for this specific entry
|
||||
if let Some(entry_uuid) = entry_model.uuid {
|
||||
if let Some(tags) = tags_by_entry.get(&entry_uuid) {
|
||||
file.tags = tags.clone();
|
||||
}
|
||||
}
|
||||
|
||||
instances.push(file);
|
||||
}
|
||||
|
||||
let total_count = instances.len() as u32;
|
||||
|
||||
Ok(AlternateInstancesOutput {
|
||||
instances,
|
||||
total_count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AlternateInstancesQuery {
|
||||
/// Resolve the full absolute SdPath for an entry
|
||||
async fn resolve_entry_path(
|
||||
&self,
|
||||
entry: &entry::Model,
|
||||
db: &DatabaseConnection,
|
||||
) -> QueryResult<SdPath> {
|
||||
// Walk up the entry hierarchy to build the full path
|
||||
let mut path_components = Vec::new();
|
||||
|
||||
// Add the file name with extension
|
||||
let file_name = if let Some(ext) = &entry.extension {
|
||||
format!("{}.{}", entry.name, ext)
|
||||
} else {
|
||||
entry.name.clone()
|
||||
};
|
||||
path_components.push(file_name);
|
||||
|
||||
// Walk up parent chain
|
||||
let mut current_parent_id = entry.parent_id;
|
||||
let mut location_entry_id = None;
|
||||
|
||||
while let Some(parent_id) = current_parent_id {
|
||||
let parent = entry::Entity::find_by_id(parent_id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| QueryError::Internal("Parent entry not found".to_string()))?;
|
||||
|
||||
// Check if this is the location root (no parent)
|
||||
if parent.parent_id.is_none() {
|
||||
location_entry_id = Some(parent.id);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add parent directory name to path
|
||||
path_components.push(parent.name.clone());
|
||||
current_parent_id = parent.parent_id;
|
||||
}
|
||||
|
||||
// Reverse to get correct order (root -> file)
|
||||
path_components.reverse();
|
||||
|
||||
// Get location info
|
||||
let location_entry_id = location_entry_id
|
||||
.ok_or_else(|| QueryError::Internal("Could not find location root".to_string()))?;
|
||||
|
||||
let location_model = location::Entity::find()
|
||||
.filter(location::Column::EntryId.eq(location_entry_id))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| QueryError::Internal("Location not found for entry".to_string()))?;
|
||||
|
||||
// Get device slug
|
||||
let device_model = device::Entity::find_by_id(location_model.device_id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| QueryError::Internal("Device not found".to_string()))?;
|
||||
|
||||
// Get location root absolute path
|
||||
let location_root_path = directory_paths::Entity::find()
|
||||
.filter(directory_paths::Column::EntryId.eq(location_entry_id))
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or_else(|| QueryError::Internal("Location root path not found".to_string()))?;
|
||||
|
||||
// Build absolute path: location_root + relative components
|
||||
let mut absolute_path = PathBuf::from(&location_root_path.path);
|
||||
for component in path_components {
|
||||
absolute_path.push(component);
|
||||
}
|
||||
|
||||
Ok(SdPath::Physical {
|
||||
device_slug: device_model.slug,
|
||||
path: absolute_path.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Register the query
|
||||
crate::register_library_query!(AlternateInstancesQuery, "files.alternate_instances");
|
||||
@@ -167,7 +167,7 @@ impl LibraryQuery for FileByIdQuery {
|
||||
};
|
||||
|
||||
// Convert to File using from_entity_model
|
||||
let mut file = File::from_entity_model(entry_model.clone(), sd_path);
|
||||
let mut file = File::from_entity_model(entry_model.clone(), sd_path.clone());
|
||||
file.sidecars = sidecars;
|
||||
file.content_identity = content_identity_domain;
|
||||
file.image_media_data = image_media;
|
||||
@@ -178,6 +178,13 @@ impl LibraryQuery for FileByIdQuery {
|
||||
file.content_kind = ci.kind;
|
||||
}
|
||||
|
||||
// Populate alternate paths (other instances of same content)
|
||||
if let Some(content_id) = entry_model.content_id {
|
||||
file.alternate_paths = self
|
||||
.get_alternate_paths(content_id, entry_model.id, db.conn())
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Load tags for this entry
|
||||
if let Some(entry_uuid) = entry_model.uuid {
|
||||
use std::collections::HashMap;
|
||||
@@ -255,6 +262,32 @@ impl LibraryQuery for FileByIdQuery {
|
||||
}
|
||||
|
||||
impl FileByIdQuery {
|
||||
/// Get alternate paths for all other entries with the same content_id
|
||||
async fn get_alternate_paths(
|
||||
&self,
|
||||
content_id: i32,
|
||||
current_entry_id: i32,
|
||||
db: &DatabaseConnection,
|
||||
) -> QueryResult<Vec<SdPath>> {
|
||||
// Find all entries with the same content_id (excluding current entry)
|
||||
let alternate_entries = entry::Entity::find()
|
||||
.filter(entry::Column::ContentId.eq(content_id))
|
||||
.filter(entry::Column::Id.ne(current_entry_id))
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let mut alternate_paths = Vec::new();
|
||||
|
||||
// Resolve path for each alternate entry
|
||||
for alt_entry in alternate_entries {
|
||||
if let Ok(alt_path) = self.resolve_file_path(&alt_entry, db).await {
|
||||
alternate_paths.push(alt_path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(alternate_paths)
|
||||
}
|
||||
|
||||
/// Resolve the full absolute SdPath for a file entry
|
||||
async fn resolve_file_path(
|
||||
&self,
|
||||
|
||||
@@ -177,7 +177,7 @@ impl LibraryQuery for FileByPathQuery {
|
||||
};
|
||||
|
||||
// Convert to File using from_entity_model
|
||||
let mut file = File::from_entity_model(entry_model, sd_path);
|
||||
let mut file = File::from_entity_model(entry_model.clone(), sd_path);
|
||||
file.sidecars = sidecars;
|
||||
file.content_identity = content_identity_domain;
|
||||
file.image_media_data = image_media;
|
||||
@@ -188,6 +188,13 @@ impl LibraryQuery for FileByPathQuery {
|
||||
file.content_kind = ci.kind;
|
||||
}
|
||||
|
||||
// Populate alternate paths (other instances of same content)
|
||||
if let Some(content_id) = entry_model.content_id {
|
||||
file.alternate_paths = self
|
||||
.get_alternate_paths(content_id, entry_model.id, db.conn())
|
||||
.await?;
|
||||
}
|
||||
|
||||
return Ok(Some(file));
|
||||
}
|
||||
|
||||
@@ -213,6 +220,91 @@ impl LibraryQuery for FileByPathQuery {
|
||||
}
|
||||
|
||||
impl FileByPathQuery {
|
||||
/// Get alternate paths for all other entries with the same content_id
|
||||
async fn get_alternate_paths(
|
||||
&self,
|
||||
content_id: i32,
|
||||
current_entry_id: i32,
|
||||
db: &DatabaseConnection,
|
||||
) -> QueryResult<Vec<SdPath>> {
|
||||
use crate::infra::db::entities::{device, directory_paths, location};
|
||||
|
||||
// Find all entries with the same content_id (excluding current entry)
|
||||
let alternate_entries = entry::Entity::find()
|
||||
.filter(entry::Column::ContentId.eq(content_id))
|
||||
.filter(entry::Column::Id.ne(current_entry_id))
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let mut alternate_paths = Vec::new();
|
||||
|
||||
// Resolve path for each alternate entry
|
||||
for alt_entry in alternate_entries {
|
||||
// Build the full path for this entry
|
||||
let mut path_components = Vec::new();
|
||||
|
||||
// Add the file name with extension
|
||||
let file_name = if let Some(ext) = &alt_entry.extension {
|
||||
format!("{}.{}", alt_entry.name, ext)
|
||||
} else {
|
||||
alt_entry.name.clone()
|
||||
};
|
||||
path_components.push(file_name);
|
||||
|
||||
// Walk up parent chain
|
||||
let mut current_parent_id = alt_entry.parent_id;
|
||||
let mut location_entry_id = None;
|
||||
|
||||
while let Some(parent_id) = current_parent_id {
|
||||
if let Some(parent) = entry::Entity::find_by_id(parent_id).one(db).await? {
|
||||
if parent.parent_id.is_none() {
|
||||
location_entry_id = Some(parent.id);
|
||||
break;
|
||||
}
|
||||
path_components.push(parent.name.clone());
|
||||
current_parent_id = parent.parent_id;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(location_entry_id) = location_entry_id {
|
||||
path_components.reverse();
|
||||
|
||||
// Get location and device info
|
||||
if let Some(location_model) = location::Entity::find()
|
||||
.filter(location::Column::EntryId.eq(location_entry_id))
|
||||
.one(db)
|
||||
.await?
|
||||
{
|
||||
if let Some(device_model) = device::Entity::find_by_id(location_model.device_id)
|
||||
.one(db)
|
||||
.await?
|
||||
{
|
||||
if let Some(location_root_path) = directory_paths::Entity::find()
|
||||
.filter(directory_paths::Column::EntryId.eq(location_entry_id))
|
||||
.one(db)
|
||||
.await?
|
||||
{
|
||||
// Build absolute path
|
||||
let mut absolute_path = PathBuf::from(&location_root_path.path);
|
||||
for component in path_components {
|
||||
absolute_path.push(component);
|
||||
}
|
||||
|
||||
alternate_paths.push(SdPath::Physical {
|
||||
device_slug: device_model.slug,
|
||||
path: absolute_path.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(alternate_paths)
|
||||
}
|
||||
|
||||
/// Find entry by SdPath
|
||||
async fn find_entry_by_sd_path(
|
||||
&self,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! File query operations
|
||||
|
||||
pub mod alternate_instances;
|
||||
pub mod content_kind_stats;
|
||||
pub mod directory_listing;
|
||||
pub mod file_by_id;
|
||||
@@ -7,6 +8,7 @@ pub mod file_by_path;
|
||||
pub mod media_listing;
|
||||
pub mod unique_to_location;
|
||||
|
||||
pub use alternate_instances::*;
|
||||
pub use content_kind_stats::*;
|
||||
pub use directory_listing::*;
|
||||
pub use file_by_id::*;
|
||||
|
||||
@@ -948,8 +948,17 @@ function SidecarItem({
|
||||
}
|
||||
|
||||
function InstancesTab({ file }: { file: File }) {
|
||||
const alternatePaths = file.alternate_paths || [];
|
||||
const allPaths = [file.sd_path, ...alternatePaths];
|
||||
// Query for alternate instances with full File data
|
||||
const instancesQuery = useNormalizedQuery<
|
||||
{ entry_uuid: string },
|
||||
{ instances: File[]; total_count: number }
|
||||
>({
|
||||
wireMethod: "query:files.alternate_instances",
|
||||
input: { entry_uuid: file?.id || "" },
|
||||
enabled: !!file?.id && !!file?.content_identity,
|
||||
});
|
||||
|
||||
const instances = instancesQuery.data?.instances || [];
|
||||
|
||||
const getPathDisplay = (sdPath: typeof file.sd_path) => {
|
||||
if ("Physical" in sdPath) {
|
||||
@@ -961,39 +970,118 @@ function InstancesTab({ file }: { file: File }) {
|
||||
}
|
||||
};
|
||||
|
||||
const getDeviceDisplay = (sdPath: typeof file.sd_path) => {
|
||||
if ("Physical" in sdPath) {
|
||||
return sdPath.Physical.device_slug || "Local Device";
|
||||
} else if ("Cloud" in sdPath) {
|
||||
return "Cloud Storage";
|
||||
} else {
|
||||
return "Content Addressed";
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
if (instancesQuery.isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8 text-xs text-sidebar-inkDull">
|
||||
Loading instances...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!file.content_identity) {
|
||||
return (
|
||||
<div className="no-scrollbar mask-fade-out flex flex-col space-y-4 overflow-x-hidden overflow-y-scroll pb-10 px-2 pt-2">
|
||||
<p className="text-xs text-sidebar-inkDull">
|
||||
This file has not been content-hashed yet. Instances will
|
||||
appear after indexing completes.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="no-scrollbar mask-fade-out flex flex-col space-y-4 overflow-x-hidden overflow-y-scroll pb-10 px-2 pt-2">
|
||||
<p className="text-xs text-sidebar-inkDull">
|
||||
All copies of this file across your devices and locations
|
||||
</p>
|
||||
|
||||
{allPaths.length === 1 ? (
|
||||
{instances.length === 0 || instances.length === 1 ? (
|
||||
<div className="flex items-center justify-center py-8 text-xs text-sidebar-inkDull">
|
||||
No alternate instances found
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{allPaths.map((sdPath, i) => (
|
||||
<div className="space-y-2.5">
|
||||
{instances.map((instance, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-2.5 bg-app-box/40 rounded-lg border border-app-line/50 space-y-2"
|
||||
className="p-2.5 bg-app-box/40 rounded-lg border border-app-line/50 hover:bg-app-box/60 transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-accent shrink-0 mt-0.5">
|
||||
<MapPin size={16} weight="bold" />
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium text-sidebar-ink truncate font-mono">
|
||||
{getPathDisplay(sdPath)}
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Thumbnail */}
|
||||
<div className="shrink-0">
|
||||
<FileComponent.Thumb
|
||||
file={instance}
|
||||
size={64}
|
||||
iconScale={0.5}
|
||||
className="rounded overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0 space-y-1.5">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium text-sidebar-ink truncate">
|
||||
{instance.name}
|
||||
{instance.extension &&
|
||||
`.${instance.extension}`}
|
||||
</div>
|
||||
<div className="text-[11px] text-sidebar-inkDull mt-0.5">
|
||||
{formatBytes(instance.size)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"size-2 rounded-full shrink-0 mt-1",
|
||||
instance.is_local
|
||||
? "bg-accent"
|
||||
: "bg-sidebar-inkDull/40",
|
||||
)}
|
||||
title={
|
||||
instance.is_local
|
||||
? "Available locally"
|
||||
: "Remote"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[11px] text-sidebar-inkDull mt-1">
|
||||
{"Physical" in sdPath && "Local Device"}
|
||||
{"Cloud" in sdPath && "Cloud Storage"}
|
||||
{"Content" in sdPath &&
|
||||
"Content Addressed"}
|
||||
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-sidebar-inkDull">
|
||||
<MapPin size={12} weight="bold" />
|
||||
<span className="truncate">
|
||||
{getDeviceDisplay(
|
||||
instance.sd_path,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] text-sidebar-inkDull/70 font-mono truncate">
|
||||
{getPathDisplay(instance.sd_path)}
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] text-sidebar-inkDull/70">
|
||||
Modified{" "}
|
||||
{formatDate(instance.modified_at)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="size-2 rounded-full shrink-0 mt-1 bg-accent" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -22,6 +22,28 @@ export type AddItemInput = { space_id: string; group_id: string | null; item_typ
|
||||
|
||||
export type AddItemOutput = { item: SpaceItem };
|
||||
|
||||
/**
|
||||
* Input for alternate instances query
|
||||
*/
|
||||
export type AlternateInstancesInput = {
|
||||
/**
|
||||
* The entry UUID to find alternates for
|
||||
*/
|
||||
entry_uuid: string };
|
||||
|
||||
/**
|
||||
* Output containing alternate instances
|
||||
*/
|
||||
export type AlternateInstancesOutput = {
|
||||
/**
|
||||
* All instances of this file (including the original)
|
||||
*/
|
||||
instances: File[];
|
||||
/**
|
||||
* Total number of instances found
|
||||
*/
|
||||
total_count: number };
|
||||
|
||||
/**
|
||||
* Represents an APFS container (physical storage with multiple volumes)
|
||||
*/
|
||||
@@ -4178,108 +4200,109 @@ success: boolean };
|
||||
// ===== API Type Unions =====
|
||||
|
||||
export type CoreAction =
|
||||
{ type: 'network.pair.cancel'; input: PairCancelInput; output: PairCancelOutput }
|
||||
| { type: 'models.whisper.delete'; input: DeleteWhisperModelInput; output: DeleteWhisperModelOutput }
|
||||
| { type: 'models.whisper.download'; input: DownloadWhisperModelInput; output: DownloadWhisperModelOutput }
|
||||
{ type: 'core.ephemeral_reset'; input: EphemeralCacheResetInput; output: EphemeralCacheResetOutput }
|
||||
| { type: 'network.spacedrop.send'; input: SpacedropSendInput; output: SpacedropSendOutput }
|
||||
| { type: 'libraries.open'; input: LibraryOpenInput; output: LibraryOpenOutput }
|
||||
| { type: 'network.pair.join'; input: PairJoinInput; output: PairJoinOutput }
|
||||
| { type: 'libraries.create'; input: LibraryCreateInput; output: LibraryCreateOutput }
|
||||
| { type: 'network.pair.generate'; input: PairGenerateInput; output: PairGenerateOutput }
|
||||
| { type: 'core.reset'; input: ResetDataInput; output: ResetDataOutput }
|
||||
| { type: 'network.pair.cancel'; input: PairCancelInput; output: PairCancelOutput }
|
||||
| { type: 'network.stop'; input: NetworkStopInput; output: NetworkStopOutput }
|
||||
| { type: 'libraries.delete'; input: LibraryDeleteInput; output: LibraryDeleteOutput }
|
||||
| { type: 'network.device.revoke'; input: DeviceRevokeInput; output: DeviceRevokeOutput }
|
||||
| { type: 'network.spacedrop.send'; input: SpacedropSendInput; output: SpacedropSendOutput }
|
||||
| { type: 'libraries.create'; input: LibraryCreateInput; output: LibraryCreateOutput }
|
||||
| { type: 'core.ephemeral_reset'; input: EphemeralCacheResetInput; output: EphemeralCacheResetOutput }
|
||||
| { type: 'network.start'; input: NetworkStartInput; output: NetworkStartOutput }
|
||||
| { type: 'models.whisper.delete'; input: DeleteWhisperModelInput; output: DeleteWhisperModelOutput }
|
||||
| { type: 'models.whisper.download'; input: DownloadWhisperModelInput; output: DownloadWhisperModelOutput }
|
||||
| { type: 'network.sync_setup'; input: LibrarySyncSetupInput; output: LibrarySyncSetupOutput }
|
||||
| { type: 'network.pair.generate'; input: PairGenerateInput; output: PairGenerateOutput }
|
||||
| { type: 'core.reset'; input: ResetDataInput; output: ResetDataOutput }
|
||||
;
|
||||
|
||||
export type LibraryAction =
|
||||
{ type: 'locations.triggerJob'; input: LocationTriggerJobInput; output: LocationTriggerJobOutput }
|
||||
| { type: 'locations.enable_indexing'; input: EnableIndexingInput; output: EnableIndexingOutput }
|
||||
| { type: 'spaces.delete_group'; input: DeleteGroupInput; output: DeleteGroupOutput }
|
||||
| { type: 'media.splat.generate'; input: GenerateSplatInput; output: GenerateSplatOutput }
|
||||
| { type: 'media.thumbnail.regenerate'; input: RegenerateThumbnailInput; output: RegenerateThumbnailOutput }
|
||||
| { type: 'media.thumbnail'; input: ThumbnailInput; output: JobReceipt }
|
||||
| { type: 'files.copy'; input: FileCopyInput; output: JobReceipt }
|
||||
| { type: 'volumes.index'; input: IndexVolumeInput; output: IndexVolumeOutput }
|
||||
| { type: 'tags.apply'; input: ApplyTagsInput; output: ApplyTagsOutput }
|
||||
| { type: 'media.thumbstrip.generate'; input: GenerateThumbstripInput; output: GenerateThumbstripOutput }
|
||||
| { type: 'locations.update'; input: LocationUpdateInput; output: LocationUpdateOutput }
|
||||
| { type: 'locations.add'; input: LocationAddInput; output: LocationAddOutput }
|
||||
| { type: 'jobs.resume'; input: JobResumeInput; output: JobResumeOutput }
|
||||
| { type: 'files.createFolder'; input: CreateFolderInput; output: CreateFolderOutput }
|
||||
| { type: 'spaces.delete'; input: SpaceDeleteInput; output: SpaceDeleteOutput }
|
||||
| { type: 'spaces.add_item'; input: AddItemInput; output: AddItemOutput }
|
||||
| { type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput }
|
||||
| { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt }
|
||||
| { type: 'volumes.refresh'; input: VolumeRefreshInput; output: VolumeRefreshOutput }
|
||||
| { type: 'volumes.remove_cloud'; input: VolumeRemoveCloudInput; output: VolumeRemoveCloudOutput }
|
||||
| { type: 'indexing.verify'; input: IndexVerifyInput; output: IndexVerifyOutput }
|
||||
| { type: 'media.ocr.extract'; input: ExtractTextInput; output: ExtractTextOutput }
|
||||
| { type: 'locations.rescan'; input: LocationRescanInput; output: LocationRescanOutput }
|
||||
| { type: 'spaces.update'; input: SpaceUpdateInput; output: SpaceUpdateOutput }
|
||||
{ type: 'tags.apply'; input: ApplyTagsInput; output: ApplyTagsOutput }
|
||||
| { type: 'spaces.create'; input: SpaceCreateInput; output: SpaceCreateOutput }
|
||||
| { type: 'files.rename'; input: FileRenameInput; output: JobReceipt }
|
||||
| { type: 'libraries.rename'; input: LibraryRenameInput; output: LibraryRenameOutput }
|
||||
| { type: 'volumes.add_cloud'; input: VolumeAddCloudInput; output: VolumeAddCloudOutput }
|
||||
| { type: 'media.proxy.generate'; input: GenerateProxyInput; output: GenerateProxyOutput }
|
||||
| { type: 'libraries.export'; input: LibraryExportInput; output: LibraryExportOutput }
|
||||
| { type: 'locations.remove'; input: LocationRemoveInput; output: LocationRemoveOutput }
|
||||
| { type: 'files.createFolder'; input: CreateFolderInput; output: CreateFolderOutput }
|
||||
| { type: 'locations.export'; input: LocationExportInput; output: LocationExportOutput }
|
||||
| { type: 'jobs.pause'; input: JobPauseInput; output: JobPauseOutput }
|
||||
| { type: 'spaces.add_item'; input: AddItemInput; output: AddItemOutput }
|
||||
| { type: 'volumes.track'; input: VolumeTrackInput; output: VolumeTrackOutput }
|
||||
| { type: 'media.speech.transcribe'; input: TranscribeAudioInput; output: TranscribeAudioOutput }
|
||||
| { type: 'indexing.start'; input: IndexInput; output: JobReceipt }
|
||||
| { type: 'volumes.untrack'; input: VolumeUntrackInput; output: VolumeUntrackOutput }
|
||||
| { type: 'volumes.speed_test'; input: VolumeSpeedTestInput; output: VolumeSpeedTestOutput }
|
||||
| { type: 'tags.create'; input: CreateTagInput; output: CreateTagOutput }
|
||||
| { type: 'spaces.update_group'; input: UpdateGroupInput; output: UpdateGroupOutput }
|
||||
| { type: 'volumes.track'; input: VolumeTrackInput; output: VolumeTrackOutput }
|
||||
| { type: 'spaces.delete_item'; input: DeleteItemInput; output: DeleteItemOutput }
|
||||
| { type: 'spaces.add_group'; input: AddGroupInput; output: AddGroupOutput }
|
||||
| { type: 'media.speech.transcribe'; input: TranscribeAudioInput; output: TranscribeAudioOutput }
|
||||
| { type: 'locations.export'; input: LocationExportInput; output: LocationExportOutput }
|
||||
| { type: 'spaces.create'; input: SpaceCreateInput; output: SpaceCreateOutput }
|
||||
| { type: 'locations.import'; input: LocationImportInput; output: LocationImportOutput }
|
||||
| { type: 'jobs.pause'; input: JobPauseInput; output: JobPauseOutput }
|
||||
| { type: 'media.ocr.extract'; input: ExtractTextInput; output: ExtractTextOutput }
|
||||
| { type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput }
|
||||
| { type: 'spaces.reorder_items'; input: ReorderItemsInput; output: ReorderOutput }
|
||||
| { type: 'spaces.reorder_groups'; input: ReorderGroupsInput; output: ReorderOutput }
|
||||
| { type: 'media.proxy.generate'; input: GenerateProxyInput; output: GenerateProxyOutput }
|
||||
| { type: 'volumes.remove_cloud'; input: VolumeRemoveCloudInput; output: VolumeRemoveCloudOutput }
|
||||
| { type: 'spaces.delete_group'; input: DeleteGroupInput; output: DeleteGroupOutput }
|
||||
| { type: 'libraries.export'; input: LibraryExportInput; output: LibraryExportOutput }
|
||||
| { type: 'libraries.rename'; input: LibraryRenameInput; output: LibraryRenameOutput }
|
||||
| { type: 'spaces.update_group'; input: UpdateGroupInput; output: UpdateGroupOutput }
|
||||
| { type: 'tags.create'; input: CreateTagInput; output: CreateTagOutput }
|
||||
| { type: 'spaces.add_group'; input: AddGroupInput; output: AddGroupOutput }
|
||||
| { type: 'locations.add'; input: LocationAddInput; output: LocationAddOutput }
|
||||
| { type: 'volumes.speed_test'; input: VolumeSpeedTestInput; output: VolumeSpeedTestOutput }
|
||||
| { type: 'spaces.delete'; input: SpaceDeleteInput; output: SpaceDeleteOutput }
|
||||
| { type: 'indexing.verify'; input: IndexVerifyInput; output: IndexVerifyOutput }
|
||||
| { type: 'volumes.index'; input: IndexVolumeInput; output: IndexVolumeOutput }
|
||||
| { type: 'media.splat.generate'; input: GenerateSplatInput; output: GenerateSplatOutput }
|
||||
| { type: 'jobs.resume'; input: JobResumeInput; output: JobResumeOutput }
|
||||
| { type: 'locations.remove'; input: LocationRemoveInput; output: LocationRemoveOutput }
|
||||
| { type: 'spaces.update'; input: SpaceUpdateInput; output: SpaceUpdateOutput }
|
||||
| { type: 'locations.triggerJob'; input: LocationTriggerJobInput; output: LocationTriggerJobOutput }
|
||||
| { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt }
|
||||
| { type: 'files.copy'; input: FileCopyInput; output: JobReceipt }
|
||||
| { type: 'media.thumbnail.regenerate'; input: RegenerateThumbnailInput; output: RegenerateThumbnailOutput }
|
||||
| { type: 'media.thumbnail'; input: ThumbnailInput; output: JobReceipt }
|
||||
| { type: 'volumes.refresh'; input: VolumeRefreshInput; output: VolumeRefreshOutput }
|
||||
| { type: 'locations.update'; input: LocationUpdateInput; output: LocationUpdateOutput }
|
||||
| { type: 'locations.enable_indexing'; input: EnableIndexingInput; output: EnableIndexingOutput }
|
||||
| { type: 'locations.rescan'; input: LocationRescanInput; output: LocationRescanOutput }
|
||||
| { type: 'locations.import'; input: LocationImportInput; output: LocationImportOutput }
|
||||
| { type: 'volumes.add_cloud'; input: VolumeAddCloudInput; output: VolumeAddCloudOutput }
|
||||
| { type: 'media.thumbstrip.generate'; input: GenerateThumbstripInput; output: GenerateThumbstripOutput }
|
||||
| { type: 'spaces.delete_item'; input: DeleteItemInput; output: DeleteItemOutput }
|
||||
;
|
||||
|
||||
export type CoreQuery =
|
||||
{ type: 'core.events.list'; input: ListEventsInput; output: ListEventsOutput }
|
||||
| { type: 'core.ephemeral_status'; input: EphemeralCacheStatusInput; output: EphemeralCacheStatus }
|
||||
| { type: 'network.sync_setup.discover'; input: DiscoverRemoteLibrariesInput; output: DiscoverRemoteLibrariesOutput }
|
||||
| { type: 'models.whisper.list'; input: ListWhisperModelsInput; output: ListWhisperModelsOutput }
|
||||
| { type: 'network.pair.status'; input: PairStatusQueryInput; output: PairStatusOutput }
|
||||
| { type: 'core.status'; input: Empty; output: CoreStatus }
|
||||
| { type: 'network.status'; input: NetworkStatusQueryInput; output: NetworkStatus }
|
||||
| { type: 'network.devices.list'; input: ListPairedDevicesInput; output: ListPairedDevicesOutput }
|
||||
| { type: 'libraries.list'; input: ListLibrariesInput; output: [LibraryInfo] }
|
||||
| { type: 'jobs.remote.all_devices'; input: RemoteJobsAllDevicesInput; output: RemoteJobsAllDevicesOutput }
|
||||
{ type: 'jobs.remote.all_devices'; input: RemoteJobsAllDevicesInput; output: RemoteJobsAllDevicesOutput }
|
||||
| { type: 'jobs.remote.for_device'; input: RemoteJobsForDeviceInput; output: RemoteJobsForDeviceOutput }
|
||||
| { type: 'network.devices.list'; input: ListPairedDevicesInput; output: ListPairedDevicesOutput }
|
||||
| { type: 'network.status'; input: NetworkStatusQueryInput; output: NetworkStatus }
|
||||
| { type: 'libraries.list'; input: ListLibrariesInput; output: [LibraryInfo] }
|
||||
| { type: 'models.whisper.list'; input: ListWhisperModelsInput; output: ListWhisperModelsOutput }
|
||||
| { type: 'network.sync_setup.discover'; input: DiscoverRemoteLibrariesInput; output: DiscoverRemoteLibrariesOutput }
|
||||
| { type: 'core.events.list'; input: ListEventsInput; output: ListEventsOutput }
|
||||
| { type: 'core.status'; input: Empty; output: CoreStatus }
|
||||
| { type: 'core.ephemeral_status'; input: EphemeralCacheStatusInput; output: EphemeralCacheStatus }
|
||||
| { type: 'network.pair.status'; input: PairStatusQueryInput; output: PairStatusOutput }
|
||||
;
|
||||
|
||||
export type LibraryQuery =
|
||||
{ type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput }
|
||||
| { type: 'sync.activity'; input: GetSyncActivityInput; output: GetSyncActivityOutput }
|
||||
| { type: 'files.by_path'; input: FileByPathQuery; output: File }
|
||||
| { type: 'files.media_listing'; input: MediaListingInput; output: MediaListingOutput }
|
||||
| { type: 'files.directory_listing'; input: DirectoryListingInput; output: DirectoryListingOutput }
|
||||
| { type: 'test.ping'; input: PingInput; output: PingOutput }
|
||||
| { type: 'sync.metrics'; input: GetSyncMetricsInput; output: GetSyncMetricsOutput }
|
||||
| { type: 'sync.eventLog'; input: GetSyncEventLogInput; output: GetSyncEventLogOutput }
|
||||
| { type: 'tags.search'; input: SearchTagsInput; output: SearchTagsOutput }
|
||||
{ type: 'sync.eventLog'; input: GetSyncEventLogInput; output: GetSyncEventLogOutput }
|
||||
| { type: 'jobs.list'; input: JobListInput; output: JobListOutput }
|
||||
| { type: 'volumes.list'; input: VolumeListQueryInput; output: VolumeListOutput }
|
||||
| { type: 'devices.list'; input: ListLibraryDevicesInput; output: [Device] }
|
||||
| { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput }
|
||||
| { type: 'spaces.get_layout'; input: SpaceLayoutQueryInput; output: SpaceLayout }
|
||||
| { type: 'jobs.active'; input: ActiveJobsInput; output: ActiveJobsOutput }
|
||||
| { type: 'jobs.info'; input: JobInfoQueryInput; output: JobInfoOutput }
|
||||
| { type: 'libraries.info'; input: LibraryInfoQueryInput; output: Library }
|
||||
| { type: 'files.content_kind_stats'; input: ContentKindStatsInput; output: ContentKindStatsOutput }
|
||||
| { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput }
|
||||
| { type: 'tags.search'; input: SearchTagsInput; output: SearchTagsOutput }
|
||||
| { type: 'test.ping'; input: PingInput; output: PingOutput }
|
||||
| { type: 'sync.activity'; input: GetSyncActivityInput; output: GetSyncActivityOutput }
|
||||
| { type: 'devices.list'; input: ListLibraryDevicesInput; output: [Device] }
|
||||
| { type: 'files.by_id'; input: FileByIdQuery; output: File }
|
||||
| { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput }
|
||||
| { type: 'spaces.get_layout'; input: SpaceLayoutQueryInput; output: SpaceLayout }
|
||||
| { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput }
|
||||
| { type: 'files.media_listing'; input: MediaListingInput; output: MediaListingOutput }
|
||||
| { type: 'sync.metrics'; input: GetSyncMetricsInput; output: GetSyncMetricsOutput }
|
||||
| { type: 'files.content_kind_stats'; input: ContentKindStatsInput; output: ContentKindStatsOutput }
|
||||
| { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput }
|
||||
| { type: 'jobs.info'; input: JobInfoQueryInput; output: JobInfoOutput }
|
||||
| { type: 'files.by_path'; input: FileByPathQuery; output: File }
|
||||
| { type: 'files.unique_to_location'; input: UniqueToLocationInput; output: UniqueToLocationOutput }
|
||||
| { type: 'files.directory_listing'; input: DirectoryListingInput; output: DirectoryListingOutput }
|
||||
| { type: 'volumes.list'; input: VolumeListQueryInput; output: VolumeListOutput }
|
||||
| { type: 'spaces.list'; input: SpacesListQueryInput; output: SpacesListOutput }
|
||||
| { type: 'libraries.info'; input: LibraryInfoQueryInput; output: Library }
|
||||
| { type: 'files.alternate_instances'; input: AlternateInstancesInput; output: AlternateInstancesOutput }
|
||||
| { type: 'spaces.get'; input: SpaceGetQueryInput; output: SpaceGetOutput }
|
||||
| { type: 'locations.list'; input: LocationsListQueryInput; output: LocationsListOutput }
|
||||
;
|
||||
@@ -4288,108 +4311,109 @@ export type LibraryQuery =
|
||||
|
||||
export const WIRE_METHODS = {
|
||||
coreActions: {
|
||||
'network.pair.cancel': 'action:network.pair.cancel.input',
|
||||
'models.whisper.delete': 'action:models.whisper.delete.input',
|
||||
'models.whisper.download': 'action:models.whisper.download.input',
|
||||
'core.ephemeral_reset': 'action:core.ephemeral_reset.input',
|
||||
'network.spacedrop.send': 'action:network.spacedrop.send.input',
|
||||
'libraries.open': 'action:libraries.open.input',
|
||||
'network.pair.join': 'action:network.pair.join.input',
|
||||
'libraries.create': 'action:libraries.create.input',
|
||||
'network.pair.generate': 'action:network.pair.generate.input',
|
||||
'core.reset': 'action:core.reset.input',
|
||||
'network.pair.cancel': 'action:network.pair.cancel.input',
|
||||
'network.stop': 'action:network.stop.input',
|
||||
'libraries.delete': 'action:libraries.delete.input',
|
||||
'network.device.revoke': 'action:network.device.revoke.input',
|
||||
'network.spacedrop.send': 'action:network.spacedrop.send.input',
|
||||
'libraries.create': 'action:libraries.create.input',
|
||||
'core.ephemeral_reset': 'action:core.ephemeral_reset.input',
|
||||
'network.start': 'action:network.start.input',
|
||||
'models.whisper.delete': 'action:models.whisper.delete.input',
|
||||
'models.whisper.download': 'action:models.whisper.download.input',
|
||||
'network.sync_setup': 'action:network.sync_setup.input',
|
||||
'network.pair.generate': 'action:network.pair.generate.input',
|
||||
'core.reset': 'action:core.reset.input',
|
||||
},
|
||||
|
||||
libraryActions: {
|
||||
'locations.triggerJob': 'action:locations.triggerJob.input',
|
||||
'locations.enable_indexing': 'action:locations.enable_indexing.input',
|
||||
'spaces.delete_group': 'action:spaces.delete_group.input',
|
||||
'media.splat.generate': 'action:media.splat.generate.input',
|
||||
'media.thumbnail.regenerate': 'action:media.thumbnail.regenerate.input',
|
||||
'media.thumbnail': 'action:media.thumbnail.input',
|
||||
'files.copy': 'action:files.copy.input',
|
||||
'volumes.index': 'action:volumes.index.input',
|
||||
'tags.apply': 'action:tags.apply.input',
|
||||
'media.thumbstrip.generate': 'action:media.thumbstrip.generate.input',
|
||||
'locations.update': 'action:locations.update.input',
|
||||
'locations.add': 'action:locations.add.input',
|
||||
'jobs.resume': 'action:jobs.resume.input',
|
||||
'files.createFolder': 'action:files.createFolder.input',
|
||||
'spaces.delete': 'action:spaces.delete.input',
|
||||
'spaces.add_item': 'action:spaces.add_item.input',
|
||||
'jobs.cancel': 'action:jobs.cancel.input',
|
||||
'files.delete': 'action:files.delete.input',
|
||||
'volumes.refresh': 'action:volumes.refresh.input',
|
||||
'volumes.remove_cloud': 'action:volumes.remove_cloud.input',
|
||||
'indexing.verify': 'action:indexing.verify.input',
|
||||
'media.ocr.extract': 'action:media.ocr.extract.input',
|
||||
'locations.rescan': 'action:locations.rescan.input',
|
||||
'spaces.update': 'action:spaces.update.input',
|
||||
'spaces.create': 'action:spaces.create.input',
|
||||
'files.rename': 'action:files.rename.input',
|
||||
'libraries.rename': 'action:libraries.rename.input',
|
||||
'volumes.add_cloud': 'action:volumes.add_cloud.input',
|
||||
'media.proxy.generate': 'action:media.proxy.generate.input',
|
||||
'libraries.export': 'action:libraries.export.input',
|
||||
'locations.remove': 'action:locations.remove.input',
|
||||
'files.createFolder': 'action:files.createFolder.input',
|
||||
'locations.export': 'action:locations.export.input',
|
||||
'jobs.pause': 'action:jobs.pause.input',
|
||||
'spaces.add_item': 'action:spaces.add_item.input',
|
||||
'volumes.track': 'action:volumes.track.input',
|
||||
'media.speech.transcribe': 'action:media.speech.transcribe.input',
|
||||
'indexing.start': 'action:indexing.start.input',
|
||||
'volumes.untrack': 'action:volumes.untrack.input',
|
||||
'volumes.speed_test': 'action:volumes.speed_test.input',
|
||||
'tags.create': 'action:tags.create.input',
|
||||
'spaces.update_group': 'action:spaces.update_group.input',
|
||||
'volumes.track': 'action:volumes.track.input',
|
||||
'spaces.delete_item': 'action:spaces.delete_item.input',
|
||||
'spaces.add_group': 'action:spaces.add_group.input',
|
||||
'media.speech.transcribe': 'action:media.speech.transcribe.input',
|
||||
'locations.export': 'action:locations.export.input',
|
||||
'spaces.create': 'action:spaces.create.input',
|
||||
'locations.import': 'action:locations.import.input',
|
||||
'jobs.pause': 'action:jobs.pause.input',
|
||||
'media.ocr.extract': 'action:media.ocr.extract.input',
|
||||
'jobs.cancel': 'action:jobs.cancel.input',
|
||||
'spaces.reorder_items': 'action:spaces.reorder_items.input',
|
||||
'spaces.reorder_groups': 'action:spaces.reorder_groups.input',
|
||||
'media.proxy.generate': 'action:media.proxy.generate.input',
|
||||
'volumes.remove_cloud': 'action:volumes.remove_cloud.input',
|
||||
'spaces.delete_group': 'action:spaces.delete_group.input',
|
||||
'libraries.export': 'action:libraries.export.input',
|
||||
'libraries.rename': 'action:libraries.rename.input',
|
||||
'spaces.update_group': 'action:spaces.update_group.input',
|
||||
'tags.create': 'action:tags.create.input',
|
||||
'spaces.add_group': 'action:spaces.add_group.input',
|
||||
'locations.add': 'action:locations.add.input',
|
||||
'volumes.speed_test': 'action:volumes.speed_test.input',
|
||||
'spaces.delete': 'action:spaces.delete.input',
|
||||
'indexing.verify': 'action:indexing.verify.input',
|
||||
'volumes.index': 'action:volumes.index.input',
|
||||
'media.splat.generate': 'action:media.splat.generate.input',
|
||||
'jobs.resume': 'action:jobs.resume.input',
|
||||
'locations.remove': 'action:locations.remove.input',
|
||||
'spaces.update': 'action:spaces.update.input',
|
||||
'locations.triggerJob': 'action:locations.triggerJob.input',
|
||||
'files.delete': 'action:files.delete.input',
|
||||
'files.copy': 'action:files.copy.input',
|
||||
'media.thumbnail.regenerate': 'action:media.thumbnail.regenerate.input',
|
||||
'media.thumbnail': 'action:media.thumbnail.input',
|
||||
'volumes.refresh': 'action:volumes.refresh.input',
|
||||
'locations.update': 'action:locations.update.input',
|
||||
'locations.enable_indexing': 'action:locations.enable_indexing.input',
|
||||
'locations.rescan': 'action:locations.rescan.input',
|
||||
'locations.import': 'action:locations.import.input',
|
||||
'volumes.add_cloud': 'action:volumes.add_cloud.input',
|
||||
'media.thumbstrip.generate': 'action:media.thumbstrip.generate.input',
|
||||
'spaces.delete_item': 'action:spaces.delete_item.input',
|
||||
},
|
||||
|
||||
coreQueries: {
|
||||
'core.events.list': 'query:core.events.list',
|
||||
'core.ephemeral_status': 'query:core.ephemeral_status',
|
||||
'network.sync_setup.discover': 'query:network.sync_setup.discover',
|
||||
'models.whisper.list': 'query:models.whisper.list',
|
||||
'network.pair.status': 'query:network.pair.status',
|
||||
'core.status': 'query:core.status',
|
||||
'network.status': 'query:network.status',
|
||||
'network.devices.list': 'query:network.devices.list',
|
||||
'libraries.list': 'query:libraries.list',
|
||||
'jobs.remote.all_devices': 'query:jobs.remote.all_devices',
|
||||
'jobs.remote.for_device': 'query:jobs.remote.for_device',
|
||||
'network.devices.list': 'query:network.devices.list',
|
||||
'network.status': 'query:network.status',
|
||||
'libraries.list': 'query:libraries.list',
|
||||
'models.whisper.list': 'query:models.whisper.list',
|
||||
'network.sync_setup.discover': 'query:network.sync_setup.discover',
|
||||
'core.events.list': 'query:core.events.list',
|
||||
'core.status': 'query:core.status',
|
||||
'core.ephemeral_status': 'query:core.ephemeral_status',
|
||||
'network.pair.status': 'query:network.pair.status',
|
||||
},
|
||||
|
||||
libraryQueries: {
|
||||
'locations.suggested': 'query:locations.suggested',
|
||||
'sync.activity': 'query:sync.activity',
|
||||
'files.by_path': 'query:files.by_path',
|
||||
'files.media_listing': 'query:files.media_listing',
|
||||
'files.directory_listing': 'query:files.directory_listing',
|
||||
'test.ping': 'query:test.ping',
|
||||
'sync.metrics': 'query:sync.metrics',
|
||||
'sync.eventLog': 'query:sync.eventLog',
|
||||
'tags.search': 'query:tags.search',
|
||||
'jobs.list': 'query:jobs.list',
|
||||
'volumes.list': 'query:volumes.list',
|
||||
'devices.list': 'query:devices.list',
|
||||
'search.files': 'query:search.files',
|
||||
'spaces.get_layout': 'query:spaces.get_layout',
|
||||
'jobs.active': 'query:jobs.active',
|
||||
'jobs.info': 'query:jobs.info',
|
||||
'libraries.info': 'query:libraries.info',
|
||||
'files.content_kind_stats': 'query:files.content_kind_stats',
|
||||
'locations.validate_path': 'query:locations.validate_path',
|
||||
'tags.search': 'query:tags.search',
|
||||
'test.ping': 'query:test.ping',
|
||||
'sync.activity': 'query:sync.activity',
|
||||
'devices.list': 'query:devices.list',
|
||||
'files.by_id': 'query:files.by_id',
|
||||
'locations.validate_path': 'query:locations.validate_path',
|
||||
'spaces.get_layout': 'query:spaces.get_layout',
|
||||
'search.files': 'query:search.files',
|
||||
'files.media_listing': 'query:files.media_listing',
|
||||
'sync.metrics': 'query:sync.metrics',
|
||||
'files.content_kind_stats': 'query:files.content_kind_stats',
|
||||
'locations.suggested': 'query:locations.suggested',
|
||||
'jobs.info': 'query:jobs.info',
|
||||
'files.by_path': 'query:files.by_path',
|
||||
'files.unique_to_location': 'query:files.unique_to_location',
|
||||
'files.directory_listing': 'query:files.directory_listing',
|
||||
'volumes.list': 'query:volumes.list',
|
||||
'spaces.list': 'query:spaces.list',
|
||||
'libraries.info': 'query:libraries.info',
|
||||
'files.alternate_instances': 'query:files.alternate_instances',
|
||||
'spaces.get': 'query:spaces.get',
|
||||
'locations.list': 'query:locations.list',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user