mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-18 21:36:56 -04:00
Better Drag & Drop into OS
Now it doesn't freeze the entire app and UI, and the drag & drop into OS system is only called when the mouse leaves the app's boundaries! However, the event/system doesn't cancel when you re-enter the application, and is something I'm still working on.
This commit is contained in:
66
.zed/settings.json
Normal file
66
.zed/settings.json
Normal file
@@ -0,0 +1,66 @@
|
||||
// Folder-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
{
|
||||
"inlay_hints": {
|
||||
"enabled": false
|
||||
},
|
||||
"languages": {
|
||||
"Rust": {
|
||||
"enable_language_server": true,
|
||||
"formatter": "language_server",
|
||||
"inlay_hints": {
|
||||
"enabled": true,
|
||||
"show_type_hints": true,
|
||||
"show_parameter_hints": true,
|
||||
"show_other_hints": true,
|
||||
"show_background": false,
|
||||
"edit_debounce_ms": 700,
|
||||
"scroll_debounce_ms": 50
|
||||
}
|
||||
},
|
||||
"TOML": {
|
||||
"formatter": "language_server"
|
||||
}
|
||||
},
|
||||
"lsp": {
|
||||
"rust-analyzer": {
|
||||
"initialization_options": {
|
||||
"procMacro": {
|
||||
"enable": true
|
||||
},
|
||||
"diagnostics": {
|
||||
"experimental": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"showUnlinkedFileNotification": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"file_scan_exclusions": [
|
||||
"node_modules",
|
||||
"**/node_modules",
|
||||
"**/bower_components",
|
||||
"**/*.code-search",
|
||||
"**/*.contentlayer",
|
||||
"**/*.next",
|
||||
"**/dist",
|
||||
"apps/mobile/ios/Pods",
|
||||
"apps/mobile/android",
|
||||
"apps/mobile/ios",
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/CVS",
|
||||
"**/.DS_Store"
|
||||
],
|
||||
"format_on_save": "on",
|
||||
"ensure_final_newline_on_save": true,
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
"tab_size": 4,
|
||||
"hard_tabs": false,
|
||||
"show_whitespaces": "selection",
|
||||
"show_completion_documentation": true
|
||||
}
|
||||
BIN
Cargo.lock
generated
BIN
Cargo.lock
generated
Binary file not shown.
@@ -32,6 +32,7 @@ thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
base64 = { workspace = true }
|
||||
|
||||
# Specific Desktop dependencies
|
||||
# WARNING: Do NOT enable default features, as that vendors dbus (see below)
|
||||
@@ -45,10 +46,11 @@ tauri-plugin-http = "=2.0.3"
|
||||
tauri-plugin-os = "=2.0.1"
|
||||
tauri-plugin-shell = "=2.0.2"
|
||||
tauri-plugin-updater = "=2.0.2"
|
||||
tauri-plugin-drag = "2.0.0"
|
||||
drag = "2.0.0"
|
||||
|
||||
# memory allocator
|
||||
mimalloc = { workspace = true }
|
||||
tauri-plugin-drag = "2.0.0"
|
||||
|
||||
[dependencies.tauri]
|
||||
features = ["linux-libxdo", "macos-private-api", "native-tls-vendored", "unstable"]
|
||||
|
||||
223
apps/desktop/src-tauri/src/drag.rs
Normal file
223
apps/desktop/src-tauri/src/drag.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
// Import required dependencies for drag and drop operations, serialization, and async functionality
|
||||
use drag::{DragItem, Image, Options};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::{ipc::Channel, Manager, PhysicalPosition, State, WebviewWindow};
|
||||
|
||||
// DragState wraps a thread-safe boolean flag to track drag operation status
|
||||
#[derive(Clone)]
|
||||
pub struct DragState(pub Arc<Mutex<bool>>);
|
||||
|
||||
// Default implementation for DragState initializes with false
|
||||
impl Default for DragState {
|
||||
fn default() -> Self {
|
||||
Self(Arc::new(Mutex::new(false)))
|
||||
}
|
||||
}
|
||||
|
||||
// Enum to represent the result of a drag operation (serializable for IPC)
|
||||
#[derive(Serialize, Deserialize, Type, Clone)]
|
||||
pub enum WrappedDragResult {
|
||||
Dropped,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
// Structure to hold cursor position coordinates (serializable for IPC)
|
||||
#[derive(Serialize, Deserialize, Type, Clone)]
|
||||
pub struct WrappedCursorPosition {
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
|
||||
// Combined structure for drag operation results (serializable for IPC)
|
||||
#[derive(Serialize, Deserialize, Type, Clone)]
|
||||
pub struct CallbackResult {
|
||||
result: WrappedDragResult,
|
||||
#[serde(rename = "cursorPos")]
|
||||
cursor_pos: WrappedCursorPosition,
|
||||
}
|
||||
|
||||
// Conversion implementations for drag-rs types to our wrapped types
|
||||
impl From<drag::DragResult> for WrappedDragResult {
|
||||
fn from(result: drag::DragResult) -> Self {
|
||||
match result {
|
||||
drag::DragResult::Dropped => WrappedDragResult::Dropped,
|
||||
drag::DragResult::Cancel => WrappedDragResult::Cancel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<drag::CursorPosition> for WrappedCursorPosition {
|
||||
fn from(pos: drag::CursorPosition) -> Self {
|
||||
WrappedCursorPosition { x: pos.x, y: pos.y }
|
||||
}
|
||||
}
|
||||
|
||||
// Global flag to track if position tracking is active
|
||||
static TRACKING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[tauri::command(async)]
|
||||
/// Initiates a drag and drop operation with cursor position tracking
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `window` - The Tauri window instance
|
||||
/// * `_state` - Current drag state (unused)
|
||||
/// * `files` - Vector of file paths to be dragged
|
||||
/// * `icon_path` - Path to the preview icon for the drag operation
|
||||
/// * `on_event` - Channel for communicating drag operation events back to the frontend
|
||||
#[specta::specta]
|
||||
pub async fn start_drag(
|
||||
window: WebviewWindow,
|
||||
_state: State<'_, DragState>,
|
||||
files: Vec<String>,
|
||||
icon_path: String,
|
||||
on_event: Channel<CallbackResult>,
|
||||
) -> Result<(), String> {
|
||||
// Fast atomic swap for tracking state
|
||||
match TRACKING.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) {
|
||||
Ok(_) => {
|
||||
println!("Starting position tracking");
|
||||
}
|
||||
Err(_) => {
|
||||
// If already tracking, stop previous instance quickly
|
||||
TRACKING.store(false, Ordering::SeqCst);
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(16)).await;
|
||||
TRACKING.store(true, Ordering::SeqCst);
|
||||
println!("Restarting position tracking");
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-allocate resources before spawning task
|
||||
let window_handle = Arc::new(window);
|
||||
let app_handle = window_handle.app_handle();
|
||||
|
||||
// Initialize control flags
|
||||
let cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let is_completed = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Prepare resources once with minimal cloning
|
||||
let tracking_resources = Arc::new((files.clone(), icon_path.clone(), Arc::new(on_event)));
|
||||
|
||||
println!("Starting position tracking");
|
||||
|
||||
// Get handles for window and app management
|
||||
let window_clone = window_handle.clone();
|
||||
let app_handle_owned = app_handle.to_owned();
|
||||
let window_owned = window_clone.to_owned();
|
||||
|
||||
// Control flags for operation state
|
||||
let is_completed_clone = is_completed.clone();
|
||||
|
||||
// Spawn background task for cursor tracking
|
||||
tokio::spawn(async move {
|
||||
// Initialize tracking state
|
||||
let mut last_position = (0.0, 0.0);
|
||||
let mut last_message_time = Instant::now();
|
||||
let threshold = 1.0; // Minimum movement threshold
|
||||
let message_debounce = Duration::from_millis(32); // State update interval
|
||||
let mut was_inside = false;
|
||||
|
||||
// Main tracking loop
|
||||
while TRACKING.load(Ordering::SeqCst) && !is_completed.load(Ordering::SeqCst) {
|
||||
let window_for_check = window_owned.clone();
|
||||
// Skip if window is not focused
|
||||
if !window_for_check.is_focused().unwrap_or(false) {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(8)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get current cursor and window positions
|
||||
if let (Ok(cursor_position), Ok(window_position), Ok(window_size)) = (
|
||||
window_for_check.cursor_position(),
|
||||
window_for_check.outer_position(),
|
||||
window_for_check.inner_size(),
|
||||
) {
|
||||
// Calculate cursor position relative to window
|
||||
let relative_position = PhysicalPosition::new(
|
||||
cursor_position.x - window_position.x as f64,
|
||||
cursor_position.y - window_position.y as f64,
|
||||
);
|
||||
|
||||
// Check if cursor is inside window boundaries
|
||||
let is_inside = relative_position.x >= 0.0
|
||||
&& relative_position.y >= 0.0
|
||||
&& relative_position.x <= window_size.width as f64
|
||||
&& relative_position.y <= window_size.height as f64;
|
||||
|
||||
// Process state changes if cursor moved enough
|
||||
if is_inside != was_inside
|
||||
&& ((relative_position.x - last_position.0).abs() > threshold
|
||||
|| (relative_position.y - last_position.1).abs() > threshold)
|
||||
{
|
||||
let now = Instant::now();
|
||||
if now.duration_since(last_message_time) >= message_debounce {
|
||||
// Prepare resources for drag operation
|
||||
let files_for_drag = tracking_resources.0.clone();
|
||||
let icon_path_for_drag = tracking_resources.1.clone();
|
||||
let on_event_for_drag = tracking_resources.2.clone();
|
||||
let is_completed = is_completed_clone.clone();
|
||||
let cancel_flag_clone = cancel_flag.clone();
|
||||
let window_for_drag = window_owned.clone();
|
||||
|
||||
// Execute drag operation on main thread
|
||||
app_handle_owned
|
||||
.run_on_main_thread(move || {
|
||||
if !is_inside {
|
||||
println!("Starting drag operation");
|
||||
// Create drag items
|
||||
let paths: Vec<PathBuf> =
|
||||
files_for_drag.iter().map(PathBuf::from).collect();
|
||||
let item = DragItem::Files(paths);
|
||||
let preview_icon =
|
||||
Image::File(PathBuf::from(&icon_path_for_drag));
|
||||
|
||||
// Start the drag operation
|
||||
if let Ok(_) = drag::start_drag(
|
||||
&window_for_drag,
|
||||
item,
|
||||
preview_icon,
|
||||
move |result, cursor_pos| {
|
||||
// Send result back to frontend
|
||||
let _ = on_event_for_drag.send(CallbackResult {
|
||||
result: result.into(),
|
||||
cursor_pos: cursor_pos.into(),
|
||||
});
|
||||
// Mark operation as completed
|
||||
is_completed.store(true, Ordering::SeqCst);
|
||||
TRACKING.store(false, Ordering::SeqCst);
|
||||
},
|
||||
Options::default(),
|
||||
) {
|
||||
println!("Drag operation started");
|
||||
}
|
||||
} else {
|
||||
println!("Cursor returned to window");
|
||||
cancel_flag_clone.store(true, Ordering::SeqCst);
|
||||
// We have this for now, but technically, it doesn't do anything.
|
||||
// I'm still trying to figure out how to cancel mid-drag without the user having to cancel the dragging on the frontend too.
|
||||
// - @Rocky43007
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Update tracking state
|
||||
last_message_time = now;
|
||||
was_inside = is_inside;
|
||||
last_position = (relative_position.x, relative_position.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent excessive CPU usage
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(8)).await;
|
||||
}
|
||||
|
||||
println!("Tracking instance stopped");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -22,6 +22,7 @@ use tokio::task::block_in_place;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error};
|
||||
|
||||
mod drag;
|
||||
mod file;
|
||||
mod menu;
|
||||
mod tauri_plugins;
|
||||
@@ -200,6 +201,7 @@ async fn main() -> tauri::Result<()> {
|
||||
set_menu_bar_item_state,
|
||||
request_fda_macos,
|
||||
open_trash_in_os_explorer,
|
||||
drag::start_drag,
|
||||
file::open_file_paths,
|
||||
file::open_ephemeral_files,
|
||||
file::get_file_path_open_with_apps,
|
||||
@@ -358,11 +360,11 @@ async fn main() -> tauri::Result<()> {
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_drag::init())
|
||||
// TODO: Bring back Tauri Plugin Window State - it was buggy so we removed it.
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(updater::plugin())
|
||||
.manage(updater::State::default())
|
||||
.manage(drag::DragState::default())
|
||||
.build(tauri::generate_context!())?
|
||||
.run(|_, _| {});
|
||||
|
||||
|
||||
@@ -3,7 +3,13 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { PropsWithChildren, startTransition, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { RspcProvider, useBridgeMutation } from '@sd/client';
|
||||
import {
|
||||
getItemFilePath,
|
||||
libraryClient,
|
||||
RspcProvider,
|
||||
useBridgeMutation,
|
||||
useSelector
|
||||
} from '@sd/client';
|
||||
import {
|
||||
createRoutes,
|
||||
DeeplinkEvent,
|
||||
@@ -18,12 +24,13 @@ import { RouteTitleContext } from '@sd/interface/hooks/useRouteTitle';
|
||||
|
||||
import '@sd/ui/style';
|
||||
|
||||
import { startDrag } from '@crabnebula/tauri-plugin-drag';
|
||||
import { Channel, invoke } from '@tauri-apps/api/core';
|
||||
import SuperTokens from 'supertokens-web-js';
|
||||
import EmailPassword from 'supertokens-web-js/recipe/emailpassword';
|
||||
import Passwordless from 'supertokens-web-js/recipe/passwordless';
|
||||
import Session from 'supertokens-web-js/recipe/session';
|
||||
import ThirdParty from 'supertokens-web-js/recipe/thirdparty';
|
||||
import { explorerStore } from '@sd/interface/app/$libraryId/Explorer/store';
|
||||
// TODO: Bring this back once upstream is fixed up.
|
||||
// const client = hooks.createClient({
|
||||
// links: [
|
||||
@@ -38,6 +45,7 @@ import getWindowHandler from '@sd/interface/app/$libraryId/settings/client/accou
|
||||
import { useLocale } from '@sd/interface/hooks';
|
||||
import { AUTH_SERVER_URL, getTokens } from '@sd/interface/util';
|
||||
|
||||
import { Transparent } from '../../../packages/assets/images';
|
||||
import { commands } from './commands';
|
||||
import { platform } from './platform';
|
||||
import { queryClient } from './query';
|
||||
@@ -47,7 +55,7 @@ import { createUpdater } from './updater';
|
||||
declare global {
|
||||
interface Window {
|
||||
enableCORSFetch: (enable: boolean) => void;
|
||||
startDrag: typeof startDrag;
|
||||
useDragAndDrop: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,12 +77,89 @@ SuperTokens.init({
|
||||
|
||||
const startupError = (window as any).__SD_ERROR__ as string | undefined;
|
||||
|
||||
function useDragAndDrop() {
|
||||
const dragState = useSelector(explorerStore, (s) => s.drag);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Drag effect triggered:', {
|
||||
dragStateType: dragState?.type,
|
||||
itemCount: dragState?.type === 'dragging' ? dragState?.items?.length : undefined
|
||||
});
|
||||
|
||||
(async () => {
|
||||
if (dragState?.type === 'dragging' && dragState.items.length > 0) {
|
||||
console.log('Starting drag operation with items:', dragState.items);
|
||||
|
||||
const items = await Promise.all(
|
||||
dragState.items.map(async (item) => {
|
||||
const data = getItemFilePath(item);
|
||||
if (!data) {
|
||||
console.log('No file path data for item:', item);
|
||||
return;
|
||||
}
|
||||
|
||||
const file_path =
|
||||
'path' in data ? data.path : await libraryClient.query(['files.getPath', data.id]);
|
||||
|
||||
console.log('Resolved file path:', file_path);
|
||||
return {
|
||||
type: 'explorer-item',
|
||||
file_path: file_path
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const image = Transparent.split('/@fs')[1]!;
|
||||
console.log('Using preview image:', image);
|
||||
|
||||
const validFiles = items.filter(Boolean).map((item) => item?.file_path);
|
||||
console.log('Invoking start_drag with files:', validFiles);
|
||||
|
||||
try {
|
||||
const channel = new Channel<{
|
||||
result: 'Dropped' | 'Cancelled';
|
||||
cursorPos: { x: number; y: number };
|
||||
}>();
|
||||
|
||||
channel.onmessage = (payload) => {
|
||||
console.log('Drag completed:', {
|
||||
result: payload.result,
|
||||
position: payload.cursorPos,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (payload.result === 'Dropped') {
|
||||
console.log('Drop location:', {
|
||||
x: payload.cursorPos.x,
|
||||
y: payload.cursorPos.y,
|
||||
screen: window.screen
|
||||
});
|
||||
}
|
||||
|
||||
explorerStore.drag = null;
|
||||
};
|
||||
|
||||
await invoke('start_drag', {
|
||||
files: validFiles,
|
||||
iconPath: image,
|
||||
onEvent: channel
|
||||
});
|
||||
console.log('start_drag invoked successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to start drag:', error);
|
||||
explorerStore.drag = null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [dragState]);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
// This tells Tauri to show the current window because it's finished loading
|
||||
commands.appReady();
|
||||
window.enableCORSFetch(true);
|
||||
window.startDrag = startDrag;
|
||||
window.useDragAndDrop = useDragAndDrop;
|
||||
// .then(() => {
|
||||
// if (import.meta.env.PROD) window.fetch = fetch;
|
||||
// });
|
||||
@@ -209,6 +294,43 @@ function AppInner() {
|
||||
};
|
||||
}, [selectedTab.element]);
|
||||
|
||||
const SizeDisplay = () => {
|
||||
const [size, setSize] = useState({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
background: 'rgba(0,0,0,0.7)',
|
||||
color: 'white',
|
||||
padding: '5px 10px',
|
||||
borderRadius: '5px',
|
||||
fontSize: '12px',
|
||||
zIndex: 9999
|
||||
}}
|
||||
>
|
||||
{size.width} x {size.height}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<RouteTitleContext.Provider
|
||||
value={useMemo(
|
||||
@@ -257,8 +379,7 @@ function AppInner() {
|
||||
new Promise((res) => {
|
||||
startTransition(() => {
|
||||
setTabs((tabs) => {
|
||||
const { pathname, search } =
|
||||
selectedTab.router.state.location;
|
||||
const { pathname, search } = selectedTab.router.state.location;
|
||||
const newTab = createTab({ pathname, search });
|
||||
const newTabs = [...tabs, newTab];
|
||||
|
||||
@@ -308,6 +429,7 @@ function AppInner() {
|
||||
tab.element
|
||||
)
|
||||
)}
|
||||
<SizeDisplay />
|
||||
<div ref={ref} />
|
||||
</SpacedriveInterfaceRoot>
|
||||
</PlatformUpdaterProvider>
|
||||
|
||||
28
apps/desktop/src/commands.ts
generated
28
apps/desktop/src/commands.ts
generated
@@ -49,6 +49,31 @@ export const commands = {
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Initiates a drag and drop operation with cursor position tracking
|
||||
*
|
||||
* # Arguments
|
||||
* * `window` - The Tauri window instance
|
||||
* * `_state` - Current drag state (unused)
|
||||
* * `files` - Vector of file paths to be dragged
|
||||
* * `icon_path` - Path to the preview icon for the drag operation
|
||||
* * `on_event` - Channel for communicating drag operation events back to the frontend
|
||||
*/
|
||||
async startDrag(
|
||||
files: string[],
|
||||
iconPath: string,
|
||||
onEvent: TAURI_CHANNEL<CallbackResult>
|
||||
): Promise<Result<null, string>> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('start_drag', { files, iconPath, onEvent })
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e;
|
||||
else return { status: 'error', error: e as any };
|
||||
}
|
||||
},
|
||||
async openFilePaths(
|
||||
library: string,
|
||||
ids: number[]
|
||||
@@ -162,6 +187,7 @@ export const events = __makeEvents__<{
|
||||
/** user-defined types **/
|
||||
|
||||
export type AppThemeType = 'Auto' | 'Light' | 'Dark';
|
||||
export type CallbackResult = { result: WrappedDragResult; cursorPos: WrappedCursorPosition };
|
||||
export type DragAndDropEvent =
|
||||
| { type: 'Hovered'; paths: string[]; x: number; y: number }
|
||||
| { type: 'Dropped'; paths: string[]; x: number; y: number }
|
||||
@@ -199,6 +225,8 @@ export type RevealItem =
|
||||
| { FilePath: { id: number } }
|
||||
| { Ephemeral: { path: string } };
|
||||
export type Update = { version: string };
|
||||
export type WrappedCursorPosition = { x: number; y: number };
|
||||
export type WrappedDragResult = 'Dropped' | 'Cancel';
|
||||
|
||||
type __EventObj__<T> = {
|
||||
listen: (cb: TAURI_API_EVENT.EventCallback<T>) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
|
||||
|
||||
@@ -111,38 +111,6 @@ const ItemMetadata = memo(() => {
|
||||
const item = useGridViewItemContext();
|
||||
const { isDroppable } = useExplorerDroppableContext();
|
||||
const explorerLayout = useExplorerLayoutStore();
|
||||
const dragState = useSelector(explorerStore, (s) => s.drag);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (dragState?.type === 'dragging' && dragState.items.length > 0) {
|
||||
const items = await Promise.all(
|
||||
dragState.items.map(async (item) => {
|
||||
const data = getItemFilePath(item);
|
||||
if (!data) return;
|
||||
|
||||
const file_path =
|
||||
'path' in data
|
||||
? data.path
|
||||
: await libraryClient.query(['files.getPath', data.id]);
|
||||
|
||||
return {
|
||||
type: 'explorer-item',
|
||||
file_path: file_path
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// get image src from Transparent
|
||||
const image = Transparent.split('/@fs')[1];
|
||||
|
||||
(window as any).startDrag({
|
||||
item: items.filter(Boolean).map((item) => item?.file_path),
|
||||
icon: image
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [dragState]);
|
||||
|
||||
const isRenaming = useSelector(explorerStore, (s) => s.isRenaming && item.selected);
|
||||
|
||||
|
||||
@@ -34,6 +34,11 @@ interface Props {
|
||||
contextMenu?: () => ReactNode;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
useDragAndDrop: () => void;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This component is used in a few routes and acts as the reference demonstration of how to combine
|
||||
* all the elements of the explorer except for the context, which must be used in the parent component.
|
||||
@@ -82,6 +87,8 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
explorer.settingsStore.showHiddenFiles = !explorer.settingsStore.showHiddenFiles;
|
||||
});
|
||||
|
||||
window.useDragAndDrop();
|
||||
|
||||
useKeyRevealFinder();
|
||||
|
||||
useExplorerDnd();
|
||||
@@ -111,18 +118,13 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
contextMenu={props.contextMenu ? props.contextMenu() : <ContextMenu />}
|
||||
emptyNotice={
|
||||
props.emptyNotice ?? (
|
||||
<EmptyNotice
|
||||
icon={FolderNotchOpen}
|
||||
message="This folder is empty"
|
||||
/>
|
||||
<EmptyNotice icon={FolderNotchOpen} message="This folder is empty" />
|
||||
)
|
||||
}
|
||||
listViewOptions={{ hideHeaderBorder: true }}
|
||||
scrollPadding={{
|
||||
top: topBar.topBarHeight,
|
||||
bottom: showPathBar
|
||||
? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0)
|
||||
: undefined
|
||||
bottom: showPathBar ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) : undefined
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -142,9 +144,7 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||
)}
|
||||
style={{
|
||||
paddingTop: topBar.topBarHeight + 12,
|
||||
bottom: showPathBar
|
||||
? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0)
|
||||
: 0
|
||||
bottom: showPathBar ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) : 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user