feat: Add settings pages and configuration operations

This commit introduces new settings pages for Appearance, Advanced, Indexer, Library, Privacy, and Services. It also adds core operations for getting and updating application and library configurations.

Co-authored-by: ijamespine <ijamespine@me.com>
This commit is contained in:
Cursor Agent
2025-12-25 11:32:56 +00:00
parent 050207c3e4
commit 9597df5be2
19 changed files with 1741 additions and 137 deletions

BIN
bun.lockb
View File

Binary file not shown.

View File

@@ -0,0 +1,131 @@
//! Get app configuration query
use crate::{
config::{AppConfig, JobLoggingConfig, LoggingConfig, Preferences, ServiceConfig},
context::CoreContext,
infra::query::{CoreQuery, QueryError, QueryResult},
};
use serde::{Deserialize, Serialize};
use specta::Type;
use std::{path::PathBuf, sync::Arc};
/// Input for getting app configuration
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct GetAppConfigQueryInput;
/// App configuration response with all fields exposed
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct AppConfigOutput {
/// Config schema version
pub version: u32,
/// Data directory path
pub data_dir: PathBuf,
/// Logging level
pub log_level: String,
/// Whether telemetry is enabled
pub telemetry_enabled: bool,
/// User preferences
pub preferences: PreferencesOutput,
/// Job logging configuration
pub job_logging: JobLoggingConfigOutput,
/// Service configuration
pub services: ServiceConfigOutput,
/// Daemon logging configuration
pub logging: LoggingConfigOutput,
}
/// User preferences output
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct PreferencesOutput {
pub theme: String,
pub language: String,
}
/// Job logging configuration output
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct JobLoggingConfigOutput {
pub enabled: bool,
pub log_directory: String,
pub max_file_size: u64,
pub include_debug: bool,
pub log_ephemeral_jobs: bool,
}
/// Service configuration output
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct ServiceConfigOutput {
pub networking_enabled: bool,
pub volume_monitoring_enabled: bool,
pub fs_watcher_enabled: bool,
pub statistics_listener_enabled: bool,
}
/// Logging configuration output
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct LoggingConfigOutput {
pub main_filter: String,
}
impl From<&AppConfig> for AppConfigOutput {
fn from(config: &AppConfig) -> Self {
Self {
version: config.version,
data_dir: config.data_dir.clone(),
log_level: config.log_level.clone(),
telemetry_enabled: config.telemetry_enabled,
preferences: PreferencesOutput {
theme: config.preferences.theme.clone(),
language: config.preferences.language.clone(),
},
job_logging: JobLoggingConfigOutput {
enabled: config.job_logging.enabled,
log_directory: config.job_logging.log_directory.clone(),
max_file_size: config.job_logging.max_file_size,
include_debug: config.job_logging.include_debug,
log_ephemeral_jobs: config.job_logging.log_ephemeral_jobs,
},
services: ServiceConfigOutput {
networking_enabled: config.services.networking_enabled,
volume_monitoring_enabled: config.services.volume_monitoring_enabled,
fs_watcher_enabled: config.services.fs_watcher_enabled,
statistics_listener_enabled: config.services.statistics_listener_enabled,
},
logging: LoggingConfigOutput {
main_filter: config.logging.main_filter.clone(),
},
}
}
}
/// Query to get app configuration
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct GetAppConfigQuery;
impl CoreQuery for GetAppConfigQuery {
type Input = GetAppConfigQueryInput;
type Output = AppConfigOutput;
fn from_input(_input: Self::Input) -> QueryResult<Self> {
Ok(Self)
}
async fn execute(
self,
context: Arc<CoreContext>,
_session: crate::infra::api::SessionContext,
) -> QueryResult<Self::Output> {
let config = AppConfig::load_from(&context.data_dir)
.map_err(|e| QueryError::Internal(format!("Failed to load config: {}", e)))?;
Ok(AppConfigOutput::from(&config))
}
}
crate::register_core_query!(GetAppConfigQuery, "config.app.get");

View File

@@ -0,0 +1,9 @@
//! App configuration operations
//!
//! Operations for reading and updating daemon-wide application configuration.
pub mod get;
pub mod update;
pub use get::{GetAppConfigQuery, GetAppConfigQueryInput};
pub use update::{UpdateAppConfigAction, UpdateAppConfigInput, UpdateAppConfigOutput};

View File

@@ -0,0 +1,244 @@
//! Update app configuration action
use crate::{
config::AppConfig,
context::CoreContext,
infra::action::{error::ActionError, CoreAction, ValidationResult},
};
use serde::{Deserialize, Serialize};
use specta::Type;
use std::sync::Arc;
use tracing::info;
/// Input for updating app configuration
/// All fields are optional for partial updates
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct UpdateAppConfigInput {
/// Whether telemetry is enabled
#[serde(skip_serializing_if = "Option::is_none")]
pub telemetry_enabled: Option<bool>,
/// Logging level
#[serde(skip_serializing_if = "Option::is_none")]
pub log_level: Option<String>,
/// Theme preference (system, light, dark)
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<String>,
/// Language preference (ISO 639-1 code)
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
/// Whether networking is enabled
#[serde(skip_serializing_if = "Option::is_none")]
pub networking_enabled: Option<bool>,
/// Whether volume monitoring is enabled
#[serde(skip_serializing_if = "Option::is_none")]
pub volume_monitoring_enabled: Option<bool>,
/// Whether filesystem watcher is enabled
#[serde(skip_serializing_if = "Option::is_none")]
pub fs_watcher_enabled: Option<bool>,
/// Whether statistics listener is enabled
#[serde(skip_serializing_if = "Option::is_none")]
pub statistics_listener_enabled: Option<bool>,
/// Whether job logging is enabled
#[serde(skip_serializing_if = "Option::is_none")]
pub job_logging_enabled: Option<bool>,
/// Whether to include debug logs in job logs
#[serde(skip_serializing_if = "Option::is_none")]
pub job_logging_include_debug: Option<bool>,
}
/// Output for update app configuration action
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct UpdateAppConfigOutput {
/// Whether the update was successful
pub success: bool,
/// Message describing the result
pub message: String,
/// Whether a restart is recommended for changes to take effect
pub requires_restart: bool,
}
/// Action to update app configuration
pub struct UpdateAppConfigAction {
input: UpdateAppConfigInput,
}
impl CoreAction for UpdateAppConfigAction {
type Input = UpdateAppConfigInput;
type Output = UpdateAppConfigOutput;
fn from_input(input: Self::Input) -> Result<Self, String> {
Ok(Self { input })
}
async fn validate(
&self,
_context: Arc<CoreContext>,
) -> Result<ValidationResult, ActionError> {
// Validate log level
if let Some(ref level) = self.input.log_level {
let valid_levels = ["trace", "debug", "info", "warn", "error"];
if !valid_levels.contains(&level.to_lowercase().as_str()) {
return Err(ActionError::Validation {
field: "log_level".to_string(),
message: format!(
"Invalid log level '{}'. Must be one of: {}",
level,
valid_levels.join(", ")
),
});
}
}
// Validate theme
if let Some(ref theme) = self.input.theme {
let valid_themes = ["system", "light", "dark"];
if !valid_themes.contains(&theme.to_lowercase().as_str()) {
return Err(ActionError::Validation {
field: "theme".to_string(),
message: format!(
"Invalid theme '{}'. Must be one of: {}",
theme,
valid_themes.join(", ")
),
});
}
}
// Validate language (basic ISO 639-1 check)
if let Some(ref lang) = self.input.language {
if lang.len() != 2 || !lang.chars().all(|c| c.is_ascii_lowercase()) {
return Err(ActionError::Validation {
field: "language".to_string(),
message: "Language must be a 2-letter ISO 639-1 code (e.g., 'en', 'de')".to_string(),
});
}
}
Ok(ValidationResult::Success)
}
async fn execute(self, context: Arc<CoreContext>) -> Result<Self::Output, ActionError> {
let mut config = AppConfig::load_from(&context.data_dir)
.map_err(|e| ActionError::Internal(format!("Failed to load config: {}", e)))?;
let mut requires_restart = false;
let mut changes = Vec::new();
// Apply updates
if let Some(telemetry_enabled) = self.input.telemetry_enabled {
if config.telemetry_enabled != telemetry_enabled {
config.telemetry_enabled = telemetry_enabled;
changes.push("telemetry_enabled");
}
}
if let Some(ref log_level) = self.input.log_level {
if config.log_level != *log_level {
config.log_level = log_level.to_lowercase();
changes.push("log_level");
requires_restart = true;
}
}
if let Some(ref theme) = self.input.theme {
if config.preferences.theme != *theme {
config.preferences.theme = theme.to_lowercase();
changes.push("theme");
}
}
if let Some(ref language) = self.input.language {
if config.preferences.language != *language {
config.preferences.language = language.clone();
changes.push("language");
}
}
if let Some(networking_enabled) = self.input.networking_enabled {
if config.services.networking_enabled != networking_enabled {
config.services.networking_enabled = networking_enabled;
changes.push("networking_enabled");
requires_restart = true;
}
}
if let Some(volume_monitoring_enabled) = self.input.volume_monitoring_enabled {
if config.services.volume_monitoring_enabled != volume_monitoring_enabled {
config.services.volume_monitoring_enabled = volume_monitoring_enabled;
changes.push("volume_monitoring_enabled");
requires_restart = true;
}
}
if let Some(fs_watcher_enabled) = self.input.fs_watcher_enabled {
if config.services.fs_watcher_enabled != fs_watcher_enabled {
config.services.fs_watcher_enabled = fs_watcher_enabled;
changes.push("fs_watcher_enabled");
requires_restart = true;
}
}
if let Some(statistics_listener_enabled) = self.input.statistics_listener_enabled {
if config.services.statistics_listener_enabled != statistics_listener_enabled {
config.services.statistics_listener_enabled = statistics_listener_enabled;
changes.push("statistics_listener_enabled");
requires_restart = true;
}
}
if let Some(job_logging_enabled) = self.input.job_logging_enabled {
if config.job_logging.enabled != job_logging_enabled {
config.job_logging.enabled = job_logging_enabled;
changes.push("job_logging_enabled");
}
}
if let Some(job_logging_include_debug) = self.input.job_logging_include_debug {
if config.job_logging.include_debug != job_logging_include_debug {
config.job_logging.include_debug = job_logging_include_debug;
changes.push("job_logging_include_debug");
}
}
if changes.is_empty() {
return Ok(UpdateAppConfigOutput {
success: true,
message: "No changes to apply".to_string(),
requires_restart: false,
});
}
config
.save()
.map_err(|e| ActionError::Internal(format!("Failed to save config: {}", e)))?;
info!(
changes = ?changes,
requires_restart = requires_restart,
"App configuration updated"
);
Ok(UpdateAppConfigOutput {
success: true,
message: format!("Updated: {}", changes.join(", ")),
requires_restart,
})
}
fn action_kind(&self) -> &'static str {
"config.app.update"
}
}
crate::register_core_action!(UpdateAppConfigAction, "config.app.update");

View File

@@ -0,0 +1,128 @@
//! Get library configuration query
use crate::{
context::CoreContext,
infra::query::{LibraryQuery, QueryError, QueryResult},
library::config::{IndexerSettings, LibrarySettings},
};
use serde::{Deserialize, Serialize};
use specta::Type;
use std::sync::Arc;
/// Input for getting library configuration
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct GetLibraryConfigQueryInput;
/// Library settings output
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct LibrarySettingsOutput {
/// Whether to generate thumbnails for media files
pub generate_thumbnails: bool,
/// Thumbnail quality (0-100)
pub thumbnail_quality: u8,
/// Whether to enable AI-powered tagging
pub enable_ai_tagging: bool,
/// Whether sync is enabled for this library
pub sync_enabled: bool,
/// Whether the library is encrypted at rest
pub encryption_enabled: bool,
/// Whether to automatically track system volumes
pub auto_track_system_volumes: bool,
/// Whether to automatically track external volumes when connected
pub auto_track_external_volumes: bool,
/// Indexer settings
pub indexer: IndexerSettingsOutput,
}
/// Indexer settings output
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct IndexerSettingsOutput {
/// Skip system files
pub no_system_files: bool,
/// Skip .git repositories
pub no_git: bool,
/// Skip dev directories (node_modules, etc.)
pub no_dev_dirs: bool,
/// Skip hidden files
pub no_hidden: bool,
/// Respect .gitignore files
pub gitignore: bool,
/// Only index images
pub only_images: bool,
}
impl From<&LibrarySettings> for LibrarySettingsOutput {
fn from(settings: &LibrarySettings) -> Self {
Self {
generate_thumbnails: settings.generate_thumbnails,
thumbnail_quality: settings.thumbnail_quality,
enable_ai_tagging: settings.enable_ai_tagging,
sync_enabled: settings.sync_enabled,
encryption_enabled: settings.encryption_enabled,
auto_track_system_volumes: settings.auto_track_system_volumes,
auto_track_external_volumes: settings.auto_track_external_volumes,
indexer: IndexerSettingsOutput::from(&settings.indexer),
}
}
}
impl From<&IndexerSettings> for IndexerSettingsOutput {
fn from(settings: &IndexerSettings) -> Self {
Self {
no_system_files: settings.no_system_files,
no_git: settings.no_git,
no_dev_dirs: settings.no_dev_dirs,
no_hidden: settings.no_hidden,
gitignore: settings.gitignore,
only_images: settings.only_images,
}
}
}
/// Query to get library configuration
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct GetLibraryConfigQuery;
impl LibraryQuery for GetLibraryConfigQuery {
type Input = GetLibraryConfigQueryInput;
type Output = LibrarySettingsOutput;
fn from_input(_input: Self::Input) -> QueryResult<Self> {
Ok(Self)
}
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 selected".to_string()))?;
let library = context
.libraries()
.await
.get_library(library_id)
.await
.ok_or_else(|| QueryError::Internal("Library not found".to_string()))?;
let config = library.config().await;
Ok(LibrarySettingsOutput::from(&config.settings))
}
}
crate::register_library_query!(GetLibraryConfigQuery, "config.library.get");

View File

@@ -0,0 +1,9 @@
//! Library configuration operations
//!
//! Operations for reading and updating per-library configuration.
pub mod get;
pub mod update;
pub use get::{GetLibraryConfigQuery, GetLibraryConfigQueryInput};
pub use update::{UpdateLibraryConfigAction, UpdateLibraryConfigInput, UpdateLibraryConfigOutput};

View File

@@ -0,0 +1,244 @@
//! Update library configuration action
use crate::{
context::CoreContext,
infra::action::{error::ActionError, LibraryAction, ValidationResult},
};
use serde::{Deserialize, Serialize};
use specta::Type;
use std::sync::Arc;
use tracing::info;
/// Input for updating library configuration
/// All fields are optional for partial updates
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct UpdateLibraryConfigInput {
// Media settings
/// Whether to generate thumbnails for media files
#[serde(skip_serializing_if = "Option::is_none")]
pub generate_thumbnails: Option<bool>,
/// Thumbnail quality (1-100)
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_quality: Option<u8>,
/// Whether to enable AI-powered tagging
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_ai_tagging: Option<bool>,
// Sync & Security
/// Whether sync is enabled for this library
#[serde(skip_serializing_if = "Option::is_none")]
pub sync_enabled: Option<bool>,
/// Whether the library is encrypted at rest
#[serde(skip_serializing_if = "Option::is_none")]
pub encryption_enabled: Option<bool>,
// Auto-tracking
/// Whether to automatically track system volumes
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_track_system_volumes: Option<bool>,
/// Whether to automatically track external volumes when connected
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_track_external_volumes: Option<bool>,
// Indexer settings
/// Skip system files
#[serde(skip_serializing_if = "Option::is_none")]
pub no_system_files: Option<bool>,
/// Skip .git repositories
#[serde(skip_serializing_if = "Option::is_none")]
pub no_git: Option<bool>,
/// Skip dev directories (node_modules, etc.)
#[serde(skip_serializing_if = "Option::is_none")]
pub no_dev_dirs: Option<bool>,
/// Skip hidden files
#[serde(skip_serializing_if = "Option::is_none")]
pub no_hidden: Option<bool>,
/// Respect .gitignore files
#[serde(skip_serializing_if = "Option::is_none")]
pub gitignore: Option<bool>,
/// Only index images
#[serde(skip_serializing_if = "Option::is_none")]
pub only_images: Option<bool>,
}
/// Output for update library configuration action
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct UpdateLibraryConfigOutput {
/// Whether the update was successful
pub success: bool,
/// Message describing the result
pub message: String,
}
/// Action to update library configuration
pub struct UpdateLibraryConfigAction {
input: UpdateLibraryConfigInput,
}
impl LibraryAction for UpdateLibraryConfigAction {
type Input = UpdateLibraryConfigInput;
type Output = UpdateLibraryConfigOutput;
fn from_input(input: Self::Input) -> Result<Self, String> {
Ok(Self { input })
}
async fn validate(
&self,
_library: &Arc<crate::library::Library>,
_context: Arc<CoreContext>,
) -> Result<ValidationResult, ActionError> {
// Validate thumbnail quality
if let Some(quality) = self.input.thumbnail_quality {
if quality == 0 || quality > 100 {
return Err(ActionError::Validation {
field: "thumbnail_quality".to_string(),
message: "Thumbnail quality must be between 1 and 100".to_string(),
});
}
}
Ok(ValidationResult::Success)
}
async fn execute(
self,
library: Arc<crate::library::Library>,
_context: Arc<CoreContext>,
) -> Result<Self::Output, ActionError> {
let mut changes = Vec::new();
library
.update_config(|config| {
let settings = &mut config.settings;
if let Some(generate_thumbnails) = self.input.generate_thumbnails {
if settings.generate_thumbnails != generate_thumbnails {
settings.generate_thumbnails = generate_thumbnails;
changes.push("generate_thumbnails");
}
}
if let Some(thumbnail_quality) = self.input.thumbnail_quality {
if settings.thumbnail_quality != thumbnail_quality {
settings.thumbnail_quality = thumbnail_quality;
changes.push("thumbnail_quality");
}
}
if let Some(enable_ai_tagging) = self.input.enable_ai_tagging {
if settings.enable_ai_tagging != enable_ai_tagging {
settings.enable_ai_tagging = enable_ai_tagging;
changes.push("enable_ai_tagging");
}
}
if let Some(sync_enabled) = self.input.sync_enabled {
if settings.sync_enabled != sync_enabled {
settings.sync_enabled = sync_enabled;
changes.push("sync_enabled");
}
}
if let Some(encryption_enabled) = self.input.encryption_enabled {
if settings.encryption_enabled != encryption_enabled {
settings.encryption_enabled = encryption_enabled;
changes.push("encryption_enabled");
}
}
if let Some(auto_track_system_volumes) = self.input.auto_track_system_volumes {
if settings.auto_track_system_volumes != auto_track_system_volumes {
settings.auto_track_system_volumes = auto_track_system_volumes;
changes.push("auto_track_system_volumes");
}
}
if let Some(auto_track_external_volumes) = self.input.auto_track_external_volumes {
if settings.auto_track_external_volumes != auto_track_external_volumes {
settings.auto_track_external_volumes = auto_track_external_volumes;
changes.push("auto_track_external_volumes");
}
}
// Indexer settings
if let Some(no_system_files) = self.input.no_system_files {
if settings.indexer.no_system_files != no_system_files {
settings.indexer.no_system_files = no_system_files;
changes.push("no_system_files");
}
}
if let Some(no_git) = self.input.no_git {
if settings.indexer.no_git != no_git {
settings.indexer.no_git = no_git;
changes.push("no_git");
}
}
if let Some(no_dev_dirs) = self.input.no_dev_dirs {
if settings.indexer.no_dev_dirs != no_dev_dirs {
settings.indexer.no_dev_dirs = no_dev_dirs;
changes.push("no_dev_dirs");
}
}
if let Some(no_hidden) = self.input.no_hidden {
if settings.indexer.no_hidden != no_hidden {
settings.indexer.no_hidden = no_hidden;
changes.push("no_hidden");
}
}
if let Some(gitignore) = self.input.gitignore {
if settings.indexer.gitignore != gitignore {
settings.indexer.gitignore = gitignore;
changes.push("gitignore");
}
}
if let Some(only_images) = self.input.only_images {
if settings.indexer.only_images != only_images {
settings.indexer.only_images = only_images;
changes.push("only_images");
}
}
})
.await
.map_err(|e| ActionError::Internal(format!("Failed to update config: {}", e)))?;
if changes.is_empty() {
return Ok(UpdateLibraryConfigOutput {
success: true,
message: "No changes to apply".to_string(),
});
}
info!(
library_id = %library.id(),
changes = ?changes,
"Library configuration updated"
);
Ok(UpdateLibraryConfigOutput {
success: true,
message: format!("Updated: {}", changes.join(", ")),
})
}
fn action_kind(&self) -> &'static str {
"config.library.update"
}
}
crate::register_library_action!(UpdateLibraryConfigAction, "config.library.update");

View File

@@ -0,0 +1,6 @@
//! Configuration operations
//!
//! Operations for reading and updating application and library configuration.
pub mod app;
pub mod library;

View File

@@ -9,6 +9,7 @@
//! - Metadata operations (hierarchical tagging)
pub mod addressing;
pub mod config;
// pub mod content;
pub mod core;
pub mod devices;

View File

@@ -1,20 +1,33 @@
import { useState } from "react";
import clsx from "clsx";
import { useCoreMutation } from "../context";
import {
GeneralSettings,
AppearanceSettings,
LibrarySettings,
IndexerSettings,
ServicesSettings,
PrivacySettings,
AdvancedSettings,
AboutSettings,
} from "./pages";
interface SettingsSidebarProps {
currentPage: string;
onPageChange: (page: string) => void;
}
function SettingsSidebar({ currentPage, onPageChange }: SettingsSidebarProps) {
const sections = [
{ id: "general", label: "General" },
{ id: "library", label: "Library" },
{ id: "privacy", label: "Privacy" },
{ id: "about", label: "About" },
];
const sections = [
{ id: "general", label: "General" },
{ id: "appearance", label: "Appearance" },
{ id: "library", label: "Library" },
{ id: "indexer", label: "Indexer" },
{ id: "services", label: "Services" },
{ id: "privacy", label: "Privacy" },
{ id: "advanced", label: "Advanced" },
{ id: "about", label: "About" },
];
function SettingsSidebar({ currentPage, onPageChange }: SettingsSidebarProps) {
return (
<div className="space-y-1">
{sections.map((section) => (
@@ -43,10 +56,18 @@ function SettingsContent({ page }: SettingsContentProps) {
switch (page) {
case "general":
return <GeneralSettings />;
case "appearance":
return <AppearanceSettings />;
case "library":
return <LibrarySettings />;
case "indexer":
return <IndexerSettings />;
case "services":
return <ServicesSettings />;
case "privacy":
return <PrivacySettings />;
case "advanced":
return <AdvancedSettings />;
case "about":
return <AboutSettings />;
default:
@@ -54,135 +75,6 @@ function SettingsContent({ page }: SettingsContentProps) {
}
}
function GeneralSettings() {
const resetData = useCoreMutation("core.reset");
const handleResetData = () => {
const confirmed = window.confirm(
"Reset All Data\n\nThis will permanently delete all libraries, settings, and cached data. The app will need to be restarted. Are you sure?"
);
if (confirmed) {
resetData.mutate(
{ confirm: true },
{
onSuccess: (result) => {
alert(
result.message || "Data has been reset. Please restart the application."
);
},
onError: (error) => {
alert("Error: " + (error.message || "Failed to reset data"));
},
}
);
}
};
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">General</h2>
<p className="text-sm text-ink-dull">
Configure general application settings.
</p>
</div>
<div className="space-y-4">
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<h3 className="text-sm font-medium text-ink mb-1">Theme</h3>
<p className="text-xs text-ink-dull">Choose your preferred theme</p>
</div>
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<h3 className="text-sm font-medium text-ink mb-1">Language</h3>
<p className="text-xs text-ink-dull">Select your language</p>
</div>
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-ink mb-1">Reset All Data</h3>
<p className="text-xs text-ink-dull">
Permanently delete all libraries and settings
</p>
</div>
<button
onClick={handleResetData}
disabled={resetData.isPending}
className="px-4 py-2 bg-red-600 hover:bg-red-700 disabled:opacity-50 text-white rounded-lg text-sm font-medium transition-colors"
>
{resetData.isPending ? "Resetting..." : "Reset"}
</button>
</div>
</div>
</div>
</div>
);
}
function LibrarySettings() {
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">Library</h2>
<p className="text-sm text-ink-dull">
Manage your Spacedrive libraries.
</p>
</div>
<div className="space-y-4">
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<h3 className="text-sm font-medium text-ink mb-1">
Current Library
</h3>
<p className="text-xs text-ink-dull">View and switch libraries</p>
</div>
</div>
</div>
);
}
function PrivacySettings() {
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">Privacy</h2>
<p className="text-sm text-ink-dull">
Control your privacy and data sharing preferences.
</p>
</div>
<div className="space-y-4">
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<h3 className="text-sm font-medium text-ink mb-1">Telemetry</h3>
<p className="text-xs text-ink-dull">
Help improve Spacedrive by sharing anonymous usage data
</p>
</div>
</div>
</div>
);
}
function AboutSettings() {
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">About</h2>
<p className="text-sm text-ink-dull">
Information about Spacedrive.
</p>
</div>
<div className="space-y-4">
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<h3 className="text-sm font-medium text-ink mb-1">Version</h3>
<p className="text-xs text-ink-dull">Spacedrive v0.1.0</p>
</div>
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<h3 className="text-sm font-medium text-ink mb-1">License</h3>
<p className="text-xs text-ink-dull">AGPL-3.0</p>
</div>
</div>
</div>
);
}
export function Settings() {
const pathname = window.location.pathname;
const initialPage = pathname.split("/").filter(Boolean)[1] || "general";

View File

@@ -0,0 +1,124 @@
import { useCoreQuery } from "../../context";
export function AboutSettings() {
const { data: status } = useCoreQuery({ type: "core.status", input: {} });
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">About</h2>
<p className="text-sm text-ink-dull">
Information about Spacedrive.
</p>
</div>
{/* Branding */}
<div className="p-6 bg-app-box rounded-lg border border-app-line text-center">
<div className="flex justify-center mb-4">
<div className="w-16 h-16 bg-accent rounded-xl flex items-center justify-center">
<svg
className="w-10 h-10 text-white"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
</div>
<h3 className="text-xl font-bold text-ink mb-1">Spacedrive</h3>
<p className="text-sm text-ink-dull">
A file explorer from the future.
</p>
</div>
{/* Version Info */}
<div className="p-4 bg-app-box rounded-lg border border-app-line space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-ink">Version</span>
<span className="text-sm text-ink-dull font-mono">
{status?.version || "Loading..."}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-ink">Built</span>
<span className="text-sm text-ink-dull font-mono">
{status?.built_at || "Loading..."}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-ink">Data Directory</span>
<span className="text-sm text-ink-dull font-mono truncate max-w-[200px]">
{status?.system?.data_directory || "Loading..."}
</span>
</div>
{status?.system?.instance_name && (
<div className="flex justify-between items-center">
<span className="text-sm text-ink">Instance</span>
<span className="text-sm text-ink-dull">
{status.system.instance_name}
</span>
</div>
)}
</div>
{/* License */}
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<div className="flex justify-between items-center">
<span className="text-sm text-ink">License</span>
<a
href="https://github.com/spacedriveapp/spacedrive/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-accent hover:underline"
>
AGPL-3.0
</a>
</div>
</div>
{/* Links */}
<div className="flex flex-wrap gap-3">
<a
href="https://spacedrive.com"
target="_blank"
rel="noopener noreferrer"
className="flex-1 min-w-[120px] px-4 py-3 bg-app-box rounded-lg border border-app-line text-center hover:bg-app-hover transition-colors"
>
<span className="text-sm font-medium text-ink">Website</span>
</a>
<a
href="https://github.com/spacedriveapp/spacedrive"
target="_blank"
rel="noopener noreferrer"
className="flex-1 min-w-[120px] px-4 py-3 bg-app-box rounded-lg border border-app-line text-center hover:bg-app-hover transition-colors"
>
<span className="text-sm font-medium text-ink">GitHub</span>
</a>
<a
href="https://discord.gg/spacedrive"
target="_blank"
rel="noopener noreferrer"
className="flex-1 min-w-[120px] px-4 py-3 bg-app-box rounded-lg border border-app-line text-center hover:bg-app-hover transition-colors"
>
<span className="text-sm font-medium text-ink">Discord</span>
</a>
</div>
{/* Changelog */}
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<h3 className="text-sm font-medium text-ink mb-2">Changelog</h3>
<p className="text-xs text-ink-dull mb-2">
See what's new in the latest release.
</p>
<a
href="https://github.com/spacedriveapp/spacedrive/releases"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-accent hover:underline"
>
View Release Notes
</a>
</div>
</div>
);
}

View File

@@ -0,0 +1,88 @@
import { useForm } from "react-hook-form";
import { useCoreQuery, useCoreMutation } from "../../context";
interface AdvancedSettingsForm {
job_logging_enabled: boolean;
job_logging_include_debug: boolean;
}
export function AdvancedSettings() {
const { data: config, refetch } = useCoreQuery({ type: "config.app.get", input: {} });
const updateConfig = useCoreMutation("config.app.update");
const form = useForm<AdvancedSettingsForm>({
values: {
job_logging_enabled: config?.job_logging?.enabled ?? true,
job_logging_include_debug: config?.job_logging?.include_debug ?? false,
},
});
const onSubmit = form.handleSubmit(async (data) => {
await updateConfig.mutateAsync(data);
refetch();
});
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">Advanced</h2>
<p className="text-sm text-ink-dull">
Advanced configuration options for power users.
</p>
</div>
<div className="p-3 bg-amber-500/10 border border-amber-500/20 rounded-lg">
<p className="text-sm text-amber-400">
These settings are for expert users. Incorrect configuration may affect performance.
</p>
</div>
<form onSubmit={onSubmit} className="space-y-4">
<div className="p-4 bg-app-box rounded-lg border border-app-line space-y-4">
<h3 className="text-sm font-medium text-ink">Job Logging</h3>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Enable Job Logging</span>
<p className="text-xs text-ink-dull">Write detailed logs for background jobs</p>
</div>
<input
type="checkbox"
{...form.register("job_logging_enabled")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Include Debug Logs</span>
<p className="text-xs text-ink-dull">Include verbose debug information in job logs</p>
</div>
<input
type="checkbox"
{...form.register("job_logging_include_debug")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<div className="pt-2 border-t border-app-line">
<p className="text-xs text-ink-dull">
Job logs are stored in the library's logs directory. Enabling debug logs
will significantly increase log file sizes.
</p>
</div>
</div>
{form.formState.isDirty && (
<button
type="submit"
disabled={updateConfig.isPending}
className="px-4 py-2 bg-accent hover:bg-accent-deep text-white rounded-md text-sm font-medium transition-colors disabled:opacity-50"
>
{updateConfig.isPending ? "Saving..." : "Save Changes"}
</button>
)}
</form>
</div>
);
}

View File

@@ -0,0 +1,87 @@
import { useForm } from "react-hook-form";
import { useCoreQuery, useCoreMutation } from "../../context";
interface AppearanceSettingsForm {
theme: string;
language: string;
}
export function AppearanceSettings() {
const { data: config, refetch } = useCoreQuery({ type: "config.app.get", input: {} });
const updateConfig = useCoreMutation("config.app.update");
const form = useForm<AppearanceSettingsForm>({
values: {
theme: config?.preferences?.theme || "system",
language: config?.preferences?.language || "en",
},
});
const onSubmit = form.handleSubmit(async (data) => {
await updateConfig.mutateAsync({
theme: data.theme,
language: data.language,
});
refetch();
});
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">Appearance</h2>
<p className="text-sm text-ink-dull">
Customize how Spacedrive looks.
</p>
</div>
<form onSubmit={onSubmit} className="space-y-4">
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<label className="block">
<span className="text-sm font-medium text-ink mb-1 block">Theme</span>
<p className="text-xs text-ink-dull mb-2">Choose your preferred color theme</p>
<select
{...form.register("theme")}
className="w-full px-3 py-2 bg-app border border-app-line rounded-md text-ink text-sm focus:outline-none focus:ring-2 focus:ring-accent"
>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
</div>
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<label className="block">
<span className="text-sm font-medium text-ink mb-1 block">Language</span>
<p className="text-xs text-ink-dull mb-2">Select your preferred language</p>
<select
{...form.register("language")}
className="w-full px-3 py-2 bg-app border border-app-line rounded-md text-ink text-sm focus:outline-none focus:ring-2 focus:ring-accent"
>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="it">Italiano</option>
<option value="ja"></option>
<option value="ko"></option>
<option value="pt">Português</option>
<option value="ru">Русский</option>
<option value="zh"></option>
</select>
</label>
</div>
{form.formState.isDirty && (
<button
type="submit"
disabled={updateConfig.isPending}
className="px-4 py-2 bg-accent hover:bg-accent-deep text-white rounded-md text-sm font-medium transition-colors disabled:opacity-50"
>
{updateConfig.isPending ? "Saving..." : "Save Changes"}
</button>
)}
</form>
</div>
);
}

View File

@@ -0,0 +1,122 @@
import { useForm } from "react-hook-form";
import { useCoreQuery, useCoreMutation } from "../../context";
interface GeneralSettingsForm {
log_level: string;
}
export function GeneralSettings() {
const { data: status } = useCoreQuery({ type: "core.status", input: {} });
const { data: config, refetch } = useCoreQuery({ type: "config.app.get", input: {} });
const updateConfig = useCoreMutation("config.app.update");
const resetData = useCoreMutation("core.reset");
const form = useForm<GeneralSettingsForm>({
values: {
log_level: config?.log_level || "info",
},
});
const onSubmit = form.handleSubmit(async (data) => {
await updateConfig.mutateAsync({
log_level: data.log_level,
});
refetch();
});
const handleResetData = () => {
const confirmed = window.confirm(
"Reset All Data\n\nThis will permanently delete all libraries, settings, and cached data. The app will need to be restarted. Are you sure?"
);
if (confirmed) {
resetData.mutate(
{ confirm: true },
{
onSuccess: (result) => {
alert(
result.message || "Data has been reset. Please restart the application."
);
},
onError: (error) => {
alert("Error: " + (error.message || "Failed to reset data"));
},
}
);
}
};
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">General</h2>
<p className="text-sm text-ink-dull">
Configure general application settings.
</p>
</div>
<form onSubmit={onSubmit} className="space-y-4">
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<label className="block">
<span className="text-sm font-medium text-ink mb-1 block">Log Level</span>
<p className="text-xs text-ink-dull mb-2">Set the verbosity of daemon logs</p>
<select
{...form.register("log_level")}
className="w-full px-3 py-2 bg-app border border-app-line rounded-md text-ink text-sm focus:outline-none focus:ring-2 focus:ring-accent"
>
<option value="trace">Trace</option>
<option value="debug">Debug</option>
<option value="info">Info</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
</label>
{form.formState.isDirty && (
<button
type="submit"
disabled={updateConfig.isPending}
className="mt-3 px-4 py-2 bg-accent hover:bg-accent-deep text-white rounded-md text-sm font-medium transition-colors disabled:opacity-50"
>
{updateConfig.isPending ? "Saving..." : "Save"}
</button>
)}
</div>
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<h3 className="text-sm font-medium text-ink mb-1">Data Directory</h3>
<p className="text-xs text-ink-dull mb-2">Where Spacedrive stores its data</p>
<code className="block text-xs text-ink-dull bg-app rounded px-2 py-1 overflow-x-auto">
{config?.data_dir || status?.system?.data_directory || "Loading..."}
</code>
</div>
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<h3 className="text-sm font-medium text-ink mb-1">Instance Name</h3>
<p className="text-xs text-ink-dull mb-2">Name of this Spacedrive instance</p>
<span className="text-sm text-ink">
{status?.system?.instance_name || status?.device_info?.name || "Default Instance"}
</span>
</div>
<div className="p-4 bg-app-box rounded-lg border border-app-line">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-ink mb-1">Reset All Data</h3>
<p className="text-xs text-ink-dull">
Permanently delete all libraries and settings
</p>
</div>
<button
type="button"
onClick={handleResetData}
disabled={resetData.isPending}
className="px-4 py-2 bg-red-600 hover:bg-red-700 disabled:opacity-50 text-white rounded-lg text-sm font-medium transition-colors"
>
{resetData.isPending ? "Resetting..." : "Reset"}
</button>
</div>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,150 @@
import { useForm } from "react-hook-form";
import { useLibraryQuery, useLibraryMutation } from "../../context";
interface IndexerSettingsForm {
no_system_files: boolean;
no_git: boolean;
no_dev_dirs: boolean;
no_hidden: boolean;
gitignore: boolean;
only_images: boolean;
}
export function IndexerSettings() {
const { data: config, refetch } = useLibraryQuery({ type: "config.library.get", input: {} });
const updateConfig = useLibraryMutation("config.library.update");
const form = useForm<IndexerSettingsForm>({
values: {
no_system_files: config?.indexer?.no_system_files ?? true,
no_git: config?.indexer?.no_git ?? true,
no_dev_dirs: config?.indexer?.no_dev_dirs ?? true,
no_hidden: config?.indexer?.no_hidden ?? false,
gitignore: config?.indexer?.gitignore ?? true,
only_images: config?.indexer?.only_images ?? false,
},
});
const onSubmit = form.handleSubmit(async (data) => {
await updateConfig.mutateAsync(data);
refetch();
});
if (!config) {
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">Indexer</h2>
<p className="text-sm text-ink-dull">
No library selected. Please select a library first.
</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">Indexer</h2>
<p className="text-sm text-ink-dull">
Configure what files are indexed in your library.
</p>
</div>
<form onSubmit={onSubmit} className="space-y-4">
{/* Exclusions Section */}
<div className="p-4 bg-app-box rounded-lg border border-app-line space-y-4">
<h3 className="text-sm font-medium text-ink">Exclusions</h3>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Skip System Files</span>
<p className="text-xs text-ink-dull">Ignore OS system files and directories</p>
</div>
<input
type="checkbox"
{...form.register("no_system_files")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Skip Git Repositories</span>
<p className="text-xs text-ink-dull">Ignore .git directories</p>
</div>
<input
type="checkbox"
{...form.register("no_git")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Skip Dev Directories</span>
<p className="text-xs text-ink-dull">Ignore node_modules, vendor, target, etc.</p>
</div>
<input
type="checkbox"
{...form.register("no_dev_dirs")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Skip Hidden Files</span>
<p className="text-xs text-ink-dull">Ignore files starting with a dot</p>
</div>
<input
type="checkbox"
{...form.register("no_hidden")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
</div>
{/* Filters Section */}
<div className="p-4 bg-app-box rounded-lg border border-app-line space-y-4">
<h3 className="text-sm font-medium text-ink">Filters</h3>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Respect .gitignore</span>
<p className="text-xs text-ink-dull">Honor .gitignore files when indexing</p>
</div>
<input
type="checkbox"
{...form.register("gitignore")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Only Index Images</span>
<p className="text-xs text-ink-dull">Only index image files (photos, graphics)</p>
</div>
<input
type="checkbox"
{...form.register("only_images")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
</div>
{form.formState.isDirty && (
<button
type="submit"
disabled={updateConfig.isPending}
className="px-4 py-2 bg-accent hover:bg-accent-deep text-white rounded-md text-sm font-medium transition-colors disabled:opacity-50"
>
{updateConfig.isPending ? "Saving..." : "Save Changes"}
</button>
)}
</form>
</div>
);
}

View File

@@ -0,0 +1,172 @@
import { useForm } from "react-hook-form";
import { useLibraryQuery, useLibraryMutation } from "../../context";
interface LibrarySettingsForm {
generate_thumbnails: boolean;
thumbnail_quality: number;
enable_ai_tagging: boolean;
sync_enabled: boolean;
encryption_enabled: boolean;
auto_track_system_volumes: boolean;
auto_track_external_volumes: boolean;
}
export function LibrarySettings() {
const { data: config, refetch } = useLibraryQuery({ type: "config.library.get", input: {} });
const updateConfig = useLibraryMutation("config.library.update");
const form = useForm<LibrarySettingsForm>({
values: {
generate_thumbnails: config?.generate_thumbnails ?? true,
thumbnail_quality: config?.thumbnail_quality ?? 85,
enable_ai_tagging: config?.enable_ai_tagging ?? false,
sync_enabled: config?.sync_enabled ?? false,
encryption_enabled: config?.encryption_enabled ?? false,
auto_track_system_volumes: config?.auto_track_system_volumes ?? true,
auto_track_external_volumes: config?.auto_track_external_volumes ?? false,
},
});
const onSubmit = form.handleSubmit(async (data) => {
await updateConfig.mutateAsync(data);
refetch();
});
if (!config) {
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">Library</h2>
<p className="text-sm text-ink-dull">
No library selected. Please select a library first.
</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">Library</h2>
<p className="text-sm text-ink-dull">
Configure settings for the current library.
</p>
</div>
<form onSubmit={onSubmit} className="space-y-4">
{/* Media Section */}
<div className="p-4 bg-app-box rounded-lg border border-app-line space-y-4">
<h3 className="text-sm font-medium text-ink">Media</h3>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Generate Thumbnails</span>
<p className="text-xs text-ink-dull">Create preview images for media files</p>
</div>
<input
type="checkbox"
{...form.register("generate_thumbnails")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<label className="block">
<span className="text-sm text-ink mb-1 block">Thumbnail Quality</span>
<p className="text-xs text-ink-dull mb-2">Quality setting for generated thumbnails (1-100)</p>
<div className="flex items-center gap-3">
<input
type="range"
min="1"
max="100"
{...form.register("thumbnail_quality", { valueAsNumber: true })}
className="flex-1 h-2 bg-app rounded-lg appearance-none cursor-pointer accent-accent"
/>
<span className="text-sm text-ink w-8">{form.watch("thumbnail_quality")}</span>
</div>
</label>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">AI Tagging</span>
<p className="text-xs text-ink-dull">Enable AI-powered automatic tagging</p>
</div>
<input
type="checkbox"
{...form.register("enable_ai_tagging")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
</div>
{/* Sync & Security Section */}
<div className="p-4 bg-app-box rounded-lg border border-app-line space-y-4">
<h3 className="text-sm font-medium text-ink">Sync & Security</h3>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Sync Enabled</span>
<p className="text-xs text-ink-dull">Sync this library across devices</p>
</div>
<input
type="checkbox"
{...form.register("sync_enabled")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Encryption</span>
<p className="text-xs text-ink-dull">Encrypt library data at rest</p>
</div>
<input
type="checkbox"
{...form.register("encryption_enabled")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
</div>
{/* Auto-Tracking Section */}
<div className="p-4 bg-app-box rounded-lg border border-app-line space-y-4">
<h3 className="text-sm font-medium text-ink">Auto-Tracking</h3>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">System Volumes</span>
<p className="text-xs text-ink-dull">Automatically track system drives</p>
</div>
<input
type="checkbox"
{...form.register("auto_track_system_volumes")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">External Volumes</span>
<p className="text-xs text-ink-dull">Automatically track external drives when connected</p>
</div>
<input
type="checkbox"
{...form.register("auto_track_external_volumes")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
</div>
{form.formState.isDirty && (
<button
type="submit"
disabled={updateConfig.isPending}
className="px-4 py-2 bg-accent hover:bg-accent-deep text-white rounded-md text-sm font-medium transition-colors disabled:opacity-50"
>
{updateConfig.isPending ? "Saving..." : "Save Changes"}
</button>
)}
</form>
</div>
);
}

View File

@@ -0,0 +1,78 @@
import { useForm } from "react-hook-form";
import { useCoreQuery, useCoreMutation } from "../../context";
interface PrivacySettingsForm {
telemetry_enabled: boolean;
}
export function PrivacySettings() {
const { data: config, refetch } = useCoreQuery({ type: "config.app.get", input: {} });
const updateConfig = useCoreMutation("config.app.update");
const form = useForm<PrivacySettingsForm>({
values: {
telemetry_enabled: config?.telemetry_enabled ?? true,
},
});
const onSubmit = form.handleSubmit(async (data) => {
await updateConfig.mutateAsync(data);
refetch();
});
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">Privacy</h2>
<p className="text-sm text-ink-dull">
Control your privacy and data sharing preferences.
</p>
</div>
<form onSubmit={onSubmit} className="space-y-4">
<div className="p-4 bg-app-box rounded-lg border border-app-line space-y-4">
<h3 className="text-sm font-medium text-ink">Telemetry</h3>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Anonymous Usage Data</span>
<p className="text-xs text-ink-dull">
Help improve Spacedrive by sharing anonymous usage data
</p>
</div>
<input
type="checkbox"
{...form.register("telemetry_enabled")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<div className="pt-2 border-t border-app-line">
<p className="text-xs text-ink-dull">
We collect anonymous usage statistics to understand how Spacedrive is used
and to prioritize features. No personal data or file contents are ever collected.
</p>
<a
href="https://spacedrive.com/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-accent hover:underline mt-2 inline-block"
>
Read our Privacy Policy
</a>
</div>
</div>
{form.formState.isDirty && (
<button
type="submit"
disabled={updateConfig.isPending}
className="px-4 py-2 bg-accent hover:bg-accent-deep text-white rounded-md text-sm font-medium transition-colors disabled:opacity-50"
>
{updateConfig.isPending ? "Saving..." : "Save Changes"}
</button>
)}
</form>
</div>
);
}

View File

@@ -0,0 +1,111 @@
import { useForm } from "react-hook-form";
import { useCoreQuery, useCoreMutation } from "../../context";
interface ServicesSettingsForm {
networking_enabled: boolean;
volume_monitoring_enabled: boolean;
fs_watcher_enabled: boolean;
statistics_listener_enabled: boolean;
}
export function ServicesSettings() {
const { data: config, refetch } = useCoreQuery({ type: "config.app.get", input: {} });
const updateConfig = useCoreMutation("config.app.update");
const form = useForm<ServicesSettingsForm>({
values: {
networking_enabled: config?.services?.networking_enabled ?? true,
volume_monitoring_enabled: config?.services?.volume_monitoring_enabled ?? true,
fs_watcher_enabled: config?.services?.fs_watcher_enabled ?? true,
statistics_listener_enabled: config?.services?.statistics_listener_enabled ?? true,
},
});
const onSubmit = form.handleSubmit(async (data) => {
const result = await updateConfig.mutateAsync(data);
refetch();
if (result.requires_restart) {
alert("Some changes require a daemon restart to take effect.");
}
});
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ink mb-2">Services</h2>
<p className="text-sm text-ink-dull">
Configure daemon background services.
</p>
</div>
<div className="p-3 bg-amber-500/10 border border-amber-500/20 rounded-lg">
<p className="text-sm text-amber-400">
Changes to service settings may require a daemon restart to take effect.
</p>
</div>
<form onSubmit={onSubmit} className="space-y-4">
<div className="p-4 bg-app-box rounded-lg border border-app-line space-y-4">
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Networking</span>
<p className="text-xs text-ink-dull">Enable P2P networking and device pairing</p>
</div>
<input
type="checkbox"
{...form.register("networking_enabled")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Volume Monitoring</span>
<p className="text-xs text-ink-dull">Monitor for connected and disconnected volumes</p>
</div>
<input
type="checkbox"
{...form.register("volume_monitoring_enabled")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Filesystem Watcher</span>
<p className="text-xs text-ink-dull">Watch for file changes in tracked locations</p>
</div>
<input
type="checkbox"
{...form.register("fs_watcher_enabled")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
<label className="flex items-center justify-between">
<div>
<span className="text-sm text-ink">Statistics Listener</span>
<p className="text-xs text-ink-dull">Listen for and update library statistics</p>
</div>
<input
type="checkbox"
{...form.register("statistics_listener_enabled")}
className="h-4 w-4 rounded border-app-line text-accent focus:ring-accent"
/>
</label>
</div>
{form.formState.isDirty && (
<button
type="submit"
disabled={updateConfig.isPending}
className="px-4 py-2 bg-accent hover:bg-accent-deep text-white rounded-md text-sm font-medium transition-colors disabled:opacity-50"
>
{updateConfig.isPending ? "Saving..." : "Save Changes"}
</button>
)}
</form>
</div>
);
}

View File

@@ -0,0 +1,8 @@
export { GeneralSettings } from './GeneralSettings';
export { AppearanceSettings } from './AppearanceSettings';
export { LibrarySettings } from './LibrarySettings';
export { IndexerSettings } from './IndexerSettings';
export { ServicesSettings } from './ServicesSettings';
export { PrivacySettings } from './PrivacySettings';
export { AdvancedSettings } from './AdvancedSettings';
export { AboutSettings } from './AboutSettings';