| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
use std::path::{Path, PathBuf}; |
| 12 |
use std::sync::atomic::{AtomicBool, Ordering}; |
| 13 |
|
| 14 |
use tracing::warn; |
| 15 |
|
| 16 |
|
| 17 |
static DRAG_ACTIVE: AtomicBool = AtomicBool::new(false); |
| 18 |
|
| 19 |
#[cfg(target_os = "macos")] |
| 20 |
mod macos; |
| 21 |
#[cfg(target_os = "windows")] |
| 22 |
mod windows; |
| 23 |
|
| 24 |
|
| 25 |
pub struct DragFile { |
| 26 |
|
| 27 |
pub friendly_name: String, |
| 28 |
|
| 29 |
pub store_path: PathBuf, |
| 30 |
} |
| 31 |
|
| 32 |
|
| 33 |
|
| 34 |
|
| 35 |
|
| 36 |
fn prepare_files(files: &[DragFile]) -> Vec<PathBuf> { |
| 37 |
let dir = drag_dir(); |
| 38 |
|
| 39 |
|
| 40 |
let _ = std::fs::remove_dir_all(&dir); |
| 41 |
if std::fs::create_dir_all(&dir).is_err() { |
| 42 |
warn!("Failed to create drag temp dir"); |
| 43 |
return Vec::new(); |
| 44 |
} |
| 45 |
|
| 46 |
let mut result = Vec::with_capacity(files.len()); |
| 47 |
|
| 48 |
for file in files { |
| 49 |
|
| 50 |
let safe_name = file.friendly_name.replace(['/', '\\'], "_"); |
| 51 |
let safe_name = safe_name.replace("..", "_"); |
| 52 |
let safe_name = safe_name.trim().to_string(); |
| 53 |
if safe_name.is_empty() { |
| 54 |
warn!(name = %file.friendly_name, "Empty or invalid drag filename, skipping"); |
| 55 |
continue; |
| 56 |
} |
| 57 |
let mut target = dir.join(&safe_name); |
| 58 |
|
| 59 |
|
| 60 |
if target.exists() { |
| 61 |
let stem = Path::new(&safe_name) |
| 62 |
.file_stem() |
| 63 |
.unwrap_or_default() |
| 64 |
.to_string_lossy() |
| 65 |
.into_owned(); |
| 66 |
let ext = Path::new(&safe_name) |
| 67 |
.extension() |
| 68 |
.map(|e| format!(".{}", e.to_string_lossy())) |
| 69 |
.unwrap_or_default(); |
| 70 |
for n in 1..1000 { |
| 71 |
target = dir.join(format!("{stem} ({n}){ext}")); |
| 72 |
if !target.exists() { |
| 73 |
break; |
| 74 |
} |
| 75 |
} |
| 76 |
} |
| 77 |
|
| 78 |
if create_link(&file.store_path, &target).is_err() { |
| 79 |
warn!(name = %file.friendly_name, "Failed to create drag link"); |
| 80 |
continue; |
| 81 |
} |
| 82 |
|
| 83 |
result.push(target); |
| 84 |
} |
| 85 |
|
| 86 |
result |
| 87 |
} |
| 88 |
|
| 89 |
fn drag_dir() -> PathBuf { |
| 90 |
std::env::temp_dir().join(format!("audiofiles-drag-{}", std::process::id())) |
| 91 |
} |
| 92 |
|
| 93 |
|
| 94 |
|
| 95 |
fn create_link(original: &Path, link: &Path) -> std::io::Result<()> { |
| 96 |
#[cfg(unix)] |
| 97 |
{ |
| 98 |
std::os::unix::fs::symlink(original, link) |
| 99 |
} |
| 100 |
#[cfg(windows)] |
| 101 |
{ |
| 102 |
|
| 103 |
|
| 104 |
std::fs::hard_link(original, link).or_else(|_| std::fs::copy(original, link).map(|_| ())) |
| 105 |
} |
| 106 |
} |
| 107 |
|
| 108 |
|
| 109 |
|
| 110 |
|
| 111 |
|
| 112 |
|
| 113 |
|
| 114 |
#[tracing::instrument(skip_all, fields(count = files.len()))] |
| 115 |
pub fn begin_drag(files: &[DragFile]) -> bool { |
| 116 |
|
| 117 |
|
| 118 |
if DRAG_ACTIVE |
| 119 |
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) |
| 120 |
.is_err() |
| 121 |
{ |
| 122 |
return true; |
| 123 |
} |
| 124 |
|
| 125 |
let paths = prepare_files(files); |
| 126 |
if paths.is_empty() { |
| 127 |
DRAG_ACTIVE.store(false, Ordering::Release); |
| 128 |
return false; |
| 129 |
} |
| 130 |
|
| 131 |
#[cfg(target_os = "macos")] |
| 132 |
{ |
| 133 |
macos::begin_drag_session(&paths) |
| 134 |
} |
| 135 |
#[cfg(target_os = "windows")] |
| 136 |
{ |
| 137 |
let result = windows::begin_drag_session(&paths); |
| 138 |
DRAG_ACTIVE.store(false, Ordering::Release); |
| 139 |
result |
| 140 |
} |
| 141 |
#[cfg(not(any(target_os = "macos", target_os = "windows")))] |
| 142 |
{ |
| 143 |
DRAG_ACTIVE.store(false, Ordering::Release); |
| 144 |
false |
| 145 |
} |
| 146 |
} |
| 147 |
|