From 774e5b7839eeb265d3ebbc3a4af2bc91f88bcdb8 Mon Sep 17 00:00:00 2001 From: "Ericson \"Fogo\" Soares" Date: Thu, 5 Oct 2023 04:21:37 -0300 Subject: [PATCH] [ENG-1124 | ENG-1154] Ephemeral location bug: Opening files | Path should always be in present in Inspector (#1401) * New functions to open ephemeral files * Review items for ephemeral files * Open and OpenWith context menu for ephemeral paths * Some warnings * Fixing inspector * Fixing windows * Format * Messy rebase * Fix macos * Cargo fmt * Removing devtools as it can be opened with keybind * Fix macos * Separating ext for ephemeral files on inspector * Fixing bad rebase * Removing rename button from quickpreview for ephemeral files * Quick Preview for ephemeral files * Requested changes --- .vscode/launch.json | 3 +- .vscode/tasks.json | 3 +- CONTRIBUTING.md | 6 +- apps/desktop/crates/linux/src/app_info.rs | 2 +- .../crates/macos/src-swift/files.swift | 24 +- apps/desktop/crates/macos/src/lib.rs | 2 +- apps/desktop/crates/windows/src/lib.rs | 3 +- apps/desktop/src-tauri/src/file.rs | 396 ++++++---- apps/desktop/src-tauri/src/main.rs | 18 +- apps/desktop/src/App.tsx | 2 + apps/desktop/src/commands.ts | 17 +- apps/web/src/App.tsx | 1 + core/src/custom_uri/mod.rs | 48 +- core/src/location/file_path_helper/mod.rs | 2 + .../Explorer/ContextMenu/FilePath/Items.tsx | 34 +- .../ContextMenu/FilePath/OpenWith.tsx | 77 -- .../Explorer/ContextMenu/OpenWith.tsx | 120 +++ .../Explorer/ContextMenu/SharedItems.tsx | 53 +- .../Explorer/ContextMenu/context.tsx | 16 +- .../$libraryId/Explorer/ContextMenu/index.tsx | 2 +- .../$libraryId/Explorer/FilePath/Thumb.tsx | 11 +- .../$libraryId/Explorer/Inspector/index.tsx | 78 +- .../Explorer/QuickPreview/index.tsx | 132 ++-- .../app/$libraryId/Explorer/View/ViewItem.tsx | 31 +- .../app/$libraryId/Explorer/View/index.tsx | 4 +- interface/components/TextViewer/one-dark.scss | 703 +++++++++--------- .../components/TextViewer/one-light.scss | 677 ++++++++--------- interface/util/Platform.tsx | 10 +- packages/client/src/utils/explorerItem.ts | 12 +- 29 files changed, 1373 insertions(+), 1114 deletions(-) delete mode 100644 interface/app/$libraryId/Explorer/ContextMenu/FilePath/OpenWith.tsx create mode 100644 interface/app/$libraryId/Explorer/ContextMenu/OpenWith.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json index d8262a14a..411d7f5a1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,8 +17,7 @@ "problemMatcher": "$rustc" }, "env": { - "RUST_BACKTRACE": "short", - "SD_DEVTOOLS": "true" + "RUST_BACKTRACE": "short" // "RUST_LOG": "sd_core::invalidate-query=trace" }, "sourceLanguages": ["rust"], diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3abda0a16..a895b9fa6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -52,8 +52,7 @@ "--no-default-features" ], "env": { - "RUST_BACKTRACE": "short", - "SD_DEVTOOLS": "true" + "RUST_BACKTRACE": "short" // "RUST_LOG": "sd_core::invalidate-query=trace" }, "problemMatcher": ["$rustc"], diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66bb639af..fc967b827 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,11 +50,7 @@ To quickly run only the desktop app after `prep`, you can use: - `pnpm tauri dev` - If necessary, the webview devtools can be opened automatically by passing the following environment variable before starting the desktop app: - - - \[Bash]: `export SD_DEVTOOLS=1` - - - \[Powershell]: `$env:SD_DEVTOOLS=1` + If necessary, the webview devtools can be opened by pressing Ctrl + Shift + I in the desktop app. Also, the react-devtools can be launched using `pnpm dlx react-devtools`. However, it must be executed before starting the desktop app for it qto connect. diff --git a/apps/desktop/crates/linux/src/app_info.rs b/apps/desktop/crates/linux/src/app_info.rs index d982186b6..39834b95a 100644 --- a/apps/desktop/crates/linux/src/app_info.rs +++ b/apps/desktop/crates/linux/src/app_info.rs @@ -134,7 +134,7 @@ pub fn open_files_path_with(file_paths: &[impl AsRef], id: &str) -> Result }) } -pub fn open_file_path(file_path: &impl AsRef) -> Result<(), GlibError> { +pub fn open_file_path(file_path: impl AsRef) -> Result<(), GlibError> { let file_uri = GioFile::for_path(file_path).uri().to_string(); LAUNCH_CTX.with(|ctx| AppInfo::launch_default_for_uri(&file_uri.to_string(), Some(ctx))) } diff --git a/apps/desktop/crates/macos/src-swift/files.swift b/apps/desktop/crates/macos/src-swift/files.swift index 4af38e41f..f55da3c0c 100644 --- a/apps/desktop/crates/macos/src-swift/files.swift +++ b/apps/desktop/crates/macos/src-swift/files.swift @@ -18,7 +18,7 @@ class OpenWithApplication: NSObject { var id: SRString var url: SRString var icon: SRData - + init(name: SRString, id: SRString, url: SRString, icon: SRData) { self.name = name self.id = id @@ -36,13 +36,13 @@ func getOpenWithApplications(urlString: SRString) -> SRObjectArray { // Fallback on earlier versions url = URL(fileURLWithPath: urlString.toString()) } - + let appURLs: [URL] if #available(macOS 12.0, *) { appURLs = NSWorkspace.shared.urlsForApplications(toOpen: url) } else { // Fallback for macOS versions prior to 12 - + // Get type identifier from file URL let fileType: String if #available(macOS 11.0, *) { @@ -51,7 +51,7 @@ func getOpenWithApplications(urlString: SRString) -> SRObjectArray { print("Failed to fetch file type for the specified file URL") return SRObjectArray([]) } - + fileType = _fileType } else { // Fallback for macOS versions prior to 11 @@ -64,7 +64,7 @@ func getOpenWithApplications(urlString: SRString) -> SRObjectArray { } fileType = _fileType as String } - + // Locates an array of bundle identifiers for apps capable of handling a specified content type with the specified roles. guard let bundleIds = LSCopyAllRoleHandlersForContentType(fileType as CFString, LSRolesMask.all)? @@ -73,7 +73,7 @@ func getOpenWithApplications(urlString: SRString) -> SRObjectArray { print("Failed to fetch bundle IDs for the specified file type") return SRObjectArray([]) } - + // Retrieve all URLs for the app identified by a bundle id appURLs = bundleIds.compactMap { bundleId -> URL? in guard let retVal = LSCopyApplicationURLsForBundleIdentifier(bundleId as CFString, nil) else { @@ -82,19 +82,19 @@ func getOpenWithApplications(urlString: SRString) -> SRObjectArray { return retVal.takeRetainedValue() as? URL } } - + return SRObjectArray( appURLs.compactMap { url -> NSObject? in - guard !url.path.contains("/Applications/"), + guard url.path.contains("/Applications/"), let infoDict = Bundle(url: url)?.infoDictionary, let name = (infoDict["CFBundleDisplayName"] ?? infoDict["CFBundleName"]) as? String, let appId = infoDict["CFBundleIdentifier"] as? String else { return nil } - + let icon = NSWorkspace.shared.icon(forFile: url.path) - + return OpenWithApplication( name: SRString(name), id: SRString(appId), @@ -108,13 +108,13 @@ func getOpenWithApplications(urlString: SRString) -> SRObjectArray { func openFilePathsWith(filePath: SRString, withUrl: SRString) { let config = NSWorkspace.OpenConfiguration() let at = URL(fileURLWithPath: withUrl.toString()) - + // FIX-ME(HACK): The NULL split here is because I was not able to make this function accept a SRArray argument. // So, considering these are file paths, and \0 is not a valid character for a file path, // I am using it as a delimitor to allow the rust side to pass in an array of files paths to this function let fileURLs = filePath.toString().split(separator: "\0").map { filePath in URL(fileURLWithPath: String(filePath)) } - + NSWorkspace.shared.open(fileURLs, withApplicationAt: at, configuration: config) } diff --git a/apps/desktop/crates/macos/src/lib.rs b/apps/desktop/crates/macos/src/lib.rs index 7cbb1317e..0b0b03130 100644 --- a/apps/desktop/crates/macos/src/lib.rs +++ b/apps/desktop/crates/macos/src/lib.rs @@ -27,7 +27,7 @@ pub struct OpenWithApplication { swift!(pub fn get_open_with_applications(url: &SRString) -> SRObjectArray); swift!(pub(crate) fn open_file_path_with(file_url: &SRString, with_url: &SRString)); -pub fn open_file_paths_with(file_urls: &[&str], with_url: &str) { +pub fn open_file_paths_with(file_urls: &[String], with_url: &str) { let file_url = file_urls.join("\0"); unsafe { open_file_path_with(&file_url.as_str().into(), &with_url.into()) } } diff --git a/apps/desktop/crates/windows/src/lib.rs b/apps/desktop/crates/windows/src/lib.rs index 80e47f2ee..22dee47e6 100644 --- a/apps/desktop/crates/windows/src/lib.rs +++ b/apps/desktop/crates/windows/src/lib.rs @@ -93,8 +93,9 @@ pub fn list_apps_associated_with_ext(ext: &OsStr) -> Result> Ok(vec) } -pub fn open_file_path_with(path: &Path, url: &str) -> Result<()> { +pub fn open_file_path_with(path: impl AsRef, url: &str) -> Result<()> { ensure_com_initialized(); + let path = path.as_ref(); let ext = path.extension().ok_or(Error::OK)?; for handler in list_apps_associated_with_ext(ext)?.iter() { diff --git a/apps/desktop/src-tauri/src/file.rs b/apps/desktop/src-tauri/src/file.rs index f05a9d17f..b08edb601 100644 --- a/apps/desktop/src-tauri/src/file.rs +++ b/apps/desktop/src-tauri/src/file.rs @@ -1,15 +1,18 @@ use std::{ collections::{BTreeSet, HashMap, HashSet}, hash::{Hash, Hasher}, + path::PathBuf, sync::Arc, }; +use futures::future::join_all; use sd_core::{ prisma::{file_path, location}, Node, }; use serde::Serialize; use specta::Type; +use tauri::async_runtime::spawn_blocking; use tracing::error; type NodeState<'a> = tauri::State<'a, Arc>; @@ -39,11 +42,17 @@ pub async fn open_file_paths( .into_iter() .map(|(id, maybe_path)| { if let Some(path) = maybe_path { - #[cfg(target_os = "linux")] - let open_result = sd_desktop_linux::open_file_path(&path); + let open_result = { + #[cfg(target_os = "linux")] + { + sd_desktop_linux::open_file_path(path) + } - #[cfg(not(target_os = "linux"))] - let open_result = opener::open(path); + #[cfg(not(target_os = "linux"))] + { + opener::open(path) + } + }; open_result .map(|_| OpenFilePathResult::AllGood(id)) @@ -65,6 +74,39 @@ pub async fn open_file_paths( Ok(res) } +#[derive(Serialize, Type)] +#[serde(tag = "t", content = "c")] +pub enum EphemeralFileOpenResult { + Ok(PathBuf), + Err(String), +} + +#[tauri::command(async)] +#[specta::specta] +pub async fn open_ephemeral_files(paths: Vec) -> Result, ()> { + Ok(paths + .into_iter() + .map(|path| { + if let Err(e) = { + #[cfg(target_os = "linux")] + { + sd_desktop_linux::open_file_path(&path) + } + + #[cfg(not(target_os = "linux"))] + { + opener::open(&path) + } + } { + error!("Failed to open file: {e:#?}"); + EphemeralFileOpenResult::Err(e.to_string()) + } else { + EphemeralFileOpenResult::Ok(path) + } + }) + .collect()) +} + #[derive(Serialize, Type, Debug, Clone)] pub struct OpenWithApplication { url: String, @@ -85,6 +127,100 @@ impl PartialEq for OpenWithApplication { impl Eq for OpenWithApplication {} +#[cfg(target_os = "macos")] +async fn get_file_path_open_apps_set(path: PathBuf) -> Option> { + let Some(path_str) = path.to_str() else { + error!( + "File path contains non-UTF8 characters: '{}'", + path.display() + ); + return None; + }; + + let res = unsafe { sd_desktop_macos::get_open_with_applications(&path_str.into()) } + .as_slice() + .iter() + .map(|app| OpenWithApplication { + url: app.url.to_string(), + name: app.name.to_string(), + }) + .collect::>(); + + Some(res) +} + +#[cfg(target_os = "linux")] +async fn get_file_path_open_apps_set(path: PathBuf) -> Option> { + Some( + sd_desktop_linux::list_apps_associated_with_ext(&path) + .await + .into_iter() + .map(|app| OpenWithApplication { + url: app.id, + name: app.name, + }) + .collect::>(), + ) +} + +#[cfg(target_os = "windows")] +async fn get_file_path_open_apps_set(path: PathBuf) -> Option> { + let Some(ext) = path.extension() else { + error!("Failed to extract file extension for '{}'", path.display()); + return None; + }; + + sd_desktop_windows::list_apps_associated_with_ext(ext) + .map_err(|e| { + error!("{e:#?}"); + }) + .map(|handlers| { + handlers + .iter() + .filter_map(|handler| { + let (Ok(name), Ok(url)) = ( + unsafe { handler.GetUIName() } + .map_err(|e| { + error!("Error on '{}': {e:#?}", path.display()); + }) + .and_then(|name| { + unsafe { name.to_string() }.map_err(|e| { + error!("Error on '{}': {e:#?}", path.display()); + }) + }), + unsafe { handler.GetName() } + .map_err(|e| { + error!("Error on '{}': {e:#?}", path.display()); + }) + .and_then(|name| { + unsafe { name.to_string() }.map_err(|e| { + error!("Error on '{}': {e:#?}", path.display()); + }) + }), + ) else { + error!("Failed to get handler info for '{}'", path.display()); + return None; + }; + + Some(OpenWithApplication { name, url }) + }) + .collect::>() + }) + .ok() +} + +async fn aggregate_open_with_apps( + paths: impl Iterator, +) -> Result, ()> { + Ok(join_all(paths.map(get_file_path_open_apps_set)) + .await + .into_iter() + .flatten() + .reduce(|intersection, set| intersection.intersection(&set).cloned().collect()) + .map(|set| set.into_iter().collect()) + .unwrap_or(vec![])) +} + #[tauri::command(async)] #[specta::specta] pub async fn get_file_path_open_with_apps( @@ -102,123 +238,21 @@ pub async fn get_file_path_open_with_apps( return Ok(vec![]); }; - #[cfg(target_os = "macos")] - return { - Ok(paths - .into_values() - .flat_map(|path| { - let Some(path) = path.and_then(|path| path.into_os_string().into_string().ok()) - else { - error!("File not found in database"); - return None; - }; + aggregate_open_with_apps(paths.into_values().filter_map(|maybe_path| { + if maybe_path.is_none() { + error!("File not found in database"); + } + maybe_path + })) + .await +} - Some( - unsafe { sd_desktop_macos::get_open_with_applications(&path.as_str().into()) } - .as_slice() - .iter() - .map(|app| OpenWithApplication { - url: app.url.to_string(), - name: app.name.to_string(), - }) - .collect::>(), - ) - }) - .reduce(|intersection, set| intersection.intersection(&set).cloned().collect()) - .map(|set| set.into_iter().collect()) - .unwrap_or(vec![])) - }; - - #[cfg(target_os = "linux")] - { - use futures::future; - use sd_desktop_linux::list_apps_associated_with_ext; - - let apps = future::join_all(paths.into_values().map(|path| async { - let Some(path) = path else { - error!("File not found in database"); - return None; - }; - - Some( - list_apps_associated_with_ext(&path) - .await - .into_iter() - .map(|app| OpenWithApplication { - url: app.id, - name: app.name, - }) - .collect::>(), - ) - })) - .await; - - return Ok(apps - .into_iter() - .flatten() - .reduce(|intersection, set| intersection.intersection(&set).cloned().collect()) - .map(|set| set.into_iter().collect()) - .unwrap_or(vec![])); - } - - #[cfg(windows)] - return Ok(paths - .into_values() - .filter_map(|path| { - let Some(path) = path else { - error!("File not found in database"); - return None; - }; - - let Some(ext) = path.extension() else { - error!("Failed to extract file extension"); - return None; - }; - - sd_desktop_windows::list_apps_associated_with_ext(ext) - .map_err(|e| { - error!("{e:#?}"); - }) - .ok() - }) - .map(|handler| { - handler - .iter() - .filter_map(|handler| { - let (Ok(name), Ok(url)) = ( - unsafe { handler.GetUIName() } - .map_err(|e| { - error!("{e:#?}"); - }) - .and_then(|name| { - unsafe { name.to_string() }.map_err(|e| { - error!("{e:#?}"); - }) - }), - unsafe { handler.GetName() } - .map_err(|e| { - error!("{e:#?}"); - }) - .and_then(|name| { - unsafe { name.to_string() }.map_err(|e| { - error!("{e:#?}"); - }) - }), - ) else { - error!("Failed to get handler info"); - return None; - }; - - Some(OpenWithApplication { name, url }) - }) - .collect::>() - }) - .reduce(|intersection, set| intersection.intersection(&set).cloned().collect()) - .map(|set| set.into_iter().collect()) - .unwrap_or(vec![])); - - #[allow(unreachable_code)] - Ok(vec![]) +#[tauri::command(async)] +#[specta::specta] +pub async fn get_ephemeral_files_open_with_apps( + paths: Vec, +) -> Result, ()> { + aggregate_open_with_apps(paths.into_iter()).await } type FileIdAndUrl = (i32, String); @@ -248,10 +282,11 @@ pub async fn open_file_path_with( .iter() .map(|(id, path)| { let (Some(path), Some(url)) = ( - #[cfg(windows)] + #[cfg(any(target_os = "windows", target_os = "linux"))] path.as_ref(), - #[cfg(not(windows))] - path.as_ref().and_then(|path| path.to_str()), + #[cfg(target_os = "macos")] + path.as_ref() + .and_then(|path| path.to_str().map(str::to_string)), url_by_id.get(id), ) else { error!("File not found in database"); @@ -269,7 +304,7 @@ pub async fn open_file_path_with( error!("{e:#?}"); }); - #[cfg(windows)] + #[cfg(target_os = "windows")] return sd_desktop_windows::open_file_path_with(path, url).map_err(|e| { error!("{e:#?}"); }); @@ -282,10 +317,85 @@ pub async fn open_file_path_with( }) } +type PathAndUrl = (PathBuf, String); + +#[tauri::command(async)] +#[specta::specta] +pub async fn open_ephemeral_file_with(paths_and_urls: Vec) -> Result<(), ()> { + join_all( + paths_and_urls + .into_iter() + .collect::>() // Just to avoid duplicates + .into_iter() + .map(|(path, url)| async move { + #[cfg(target_os = "macos")] + if let Some(path) = path.to_str().map(str::to_string) { + if let Err(e) = spawn_blocking(move || { + sd_desktop_macos::open_file_paths_with(&[path], &url) + }) + .await + { + error!("Error joining spawned task for opening files with: {e:#?}"); + } + } else { + error!( + "File path contains non-UTF8 characters: '{}'", + path.display() + ); + }; + + #[cfg(target_os = "linux")] + match spawn_blocking(move || sd_desktop_linux::open_files_path_with(&[path], &url)) + .await + { + Ok(Ok(())) => (), + Ok(Err(e)) => error!("Error opening file with: {e:#?}"), + Err(e) => error!("Error joining spawned task for opening files with: {e:#?}"), + } + + #[cfg(windows)] + match spawn_blocking(move || sd_desktop_windows::open_file_path_with(path, &url)) + .await + { + Ok(Ok(())) => (), + Ok(Err(e)) => error!("Error opening file with: {e:#?}"), + Err(e) => error!("Error joining spawned task for opening files with: {e:#?}"), + } + }), + ) + .await; + + Ok(()) +} + +fn inner_reveal_paths(paths: impl Iterator) { + for path in paths { + #[cfg(target_os = "linux")] + if sd_desktop_linux::is_appimage() { + // This is a workaround for the app, when package inside an AppImage, crashing when using opener::reveal. + if let Err(e) = sd_desktop_linux::open_file_path(if path.is_file() { + path.parent().unwrap_or(&path) + } else { + &path + }) { + error!("Failed to open logs dir: {e:#?}"); + } + } else if let Err(e) = opener::reveal(path) { + error!("Failed to open logs dir: {e:#?}"); + } + + #[cfg(not(target_os = "linux"))] + if let Err(e) = opener::reveal(path) { + error!("Failed to open logs dir: {e:#?}"); + } + } +} + #[derive(specta::Type, serde::Deserialize)] pub enum RevealItem { Location { id: location::id::Type }, FilePath { id: file_path::id::Type }, + Ephemeral { path: PathBuf }, } #[tauri::command(async)] @@ -299,6 +409,8 @@ pub async fn reveal_items( return Err(()); }; + let mut paths_to_open = BTreeSet::new(); + let (paths, locations): (Vec<_>, Vec<_>) = items .into_iter() @@ -306,13 +418,14 @@ pub async fn reveal_items( match item { RevealItem::FilePath { id } => paths.push(id), RevealItem::Location { id } => locations.push(id), + RevealItem::Ephemeral { path } => { + paths_to_open.insert(path); + } } (paths, locations) }); - let mut paths_to_open = BTreeSet::new(); - if !paths.is_empty() { paths_to_open.extend( library @@ -343,35 +456,8 @@ pub async fn reveal_items( ); } - for path in paths_to_open { - #[cfg(target_os = "linux")] - if sd_desktop_linux::is_appimage() { - // This is a workaround for the app, when package inside an AppImage, crashing when using opener::reveal. - sd_desktop_linux::open_file_path( - &(if path.is_file() { - path.parent().unwrap_or(&path) - } else { - &path - }), - ) - .map_err(|err| { - error!("Failed to open logs dir: {err}"); - }) - .ok() - } else { - opener::reveal(path) - .map_err(|err| { - error!("Failed to open logs dir: {err}"); - }) - .ok() - }; - - #[cfg(not(target_os = "linux"))] - opener::reveal(path) - .map_err(|err| { - error!("Failed to open logs dir: {err}"); - }) - .ok(); + if let Err(e) = spawn_blocking(|| inner_reveal_paths(paths_to_open.into_iter())).await { + error!("Error joining reveal paths thread: {e:#?}"); } Ok(()) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index ae2c3b18a..df11de836 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -51,13 +51,13 @@ async fn open_logs_dir(node: tauri::State<'_, Arc>) -> Result<(), ()> { let logs_path = node.data_dir.join("logs"); #[cfg(target_os = "linux")] - let open_result = sd_desktop_linux::open_file_path(&logs_path); + let open_result = sd_desktop_linux::open_file_path(logs_path); #[cfg(not(target_os = "linux"))] let open_result = opener::open(logs_path); - open_result.map_err(|err| { - error!("Failed to open logs dir: {err}"); + open_result.map_err(|e| { + error!("Failed to open logs dir: {e:#?}"); }) } @@ -114,7 +114,7 @@ async fn main() -> tauri::Result<()> { .plugin(sd_server_plugin(node.clone()).unwrap()) // TODO: Handle `unwrap` .manage(node), Err(err) => { - error!("Error starting up the node: {err}"); + error!("Error starting up the node: {err:#?}"); app.plugin(sd_error_plugin(err)) } }; @@ -153,13 +153,6 @@ async fn main() -> tauri::Result<()> { } }); - #[cfg(debug_assertions)] - { - if std::env::var("SD_DEVTOOLS").is_ok() { - window.open_devtools(); - } - } - #[cfg(target_os = "windows")] window.set_decorations(true).unwrap(); @@ -190,8 +183,11 @@ async fn main() -> tauri::Result<()> { reset_spacedrive, open_logs_dir, file::open_file_paths, + file::open_ephemeral_files, file::get_file_path_open_with_apps, + file::get_ephemeral_files_open_with_apps, file::open_file_path_with, + file::open_ephemeral_file_with, file::reveal_items, theme::lock_app_theme ]) diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 805cfca63..b85c572fa 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -65,6 +65,8 @@ const platform: Platform = { .join('/')}.webp${queryParams}`, getFileUrl: (libraryId, locationLocalId, filePathId) => `${customUriServerUrl}file/${libraryId}/${locationLocalId}/${filePathId}${queryParams}`, + getFileUrlByPath: (path) => + `${customUriServerUrl}local-file-by-path/${encodeURIComponent(path)}${queryParams}`, openLink: shell.open, getOs, openDirectoryPickerDialog: () => dialog.open({ directory: true }), diff --git a/apps/desktop/src/commands.ts b/apps/desktop/src/commands.ts index b6d17db50..0ddfb5dff 100644 --- a/apps/desktop/src/commands.ts +++ b/apps/desktop/src/commands.ts @@ -26,14 +26,26 @@ export function openFilePaths(library: string, ids: number[]) { return invoke()("open_file_paths", { library,ids }) } +export function openEphemeralFiles(paths: string[]) { + return invoke()("open_ephemeral_files", { paths }) +} + export function getFilePathOpenWithApps(library: string, ids: number[]) { return invoke()("get_file_path_open_with_apps", { library,ids }) } +export function getEphemeralFilesOpenWithApps(paths: string[]) { + return invoke()("get_ephemeral_files_open_with_apps", { paths }) +} + export function openFilePathWith(library: string, fileIdsAndUrls: ([number, string])[]) { return invoke()("open_file_path_with", { library,fileIdsAndUrls }) } +export function openEphemeralFileWith(pathsAndUrls: ([string, string])[]) { + return invoke()("open_ephemeral_file_with", { pathsAndUrls }) +} + export function revealItems(library: string, items: RevealItem[]) { return invoke()("reveal_items", { library,items }) } @@ -43,6 +55,7 @@ export function lockAppTheme(themeType: AppThemeType) { } export type AppThemeType = "Auto" | "Light" | "Dark" -export type OpenFilePathResult = { t: "NoLibrary" } | { t: "NoFile"; c: number } | { t: "OpenError"; c: [number, string] } | { t: "AllGood"; c: number } | { t: "Internal"; c: string } -export type RevealItem = { Location: { id: number } } | { FilePath: { id: number } } +export type EphemeralFileOpenResult = { t: "Ok"; c: string } | { t: "Err"; c: string } export type OpenWithApplication = { url: string; name: string } +export type OpenFilePathResult = { t: "NoLibrary" } | { t: "NoFile"; c: number } | { t: "OpenError"; c: [number, string] } | { t: "AllGood"; c: number } | { t: "Internal"; c: string } +export type RevealItem = { Location: { id: number } } | { FilePath: { id: number } } | { Ephemeral: { path: string } } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 7860ac788..c37ababfc 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -41,6 +41,7 @@ const platform: Platform = { `${spacedriveURL}/file/${encodeURIComponent(libraryId)}/${encodeURIComponent( locationLocalId )}/${encodeURIComponent(filePathId)}`, + getFileUrlByPath: (path) => `${spacedriveURL}/local-file-by-path/${encodeURIComponent(path)}`, openLink: (url) => window.open(url, '_blank')?.focus(), confirm: (message, cb) => cb(window.confirm(message)), auth: { diff --git a/core/src/custom_uri/mod.rs b/core/src/custom_uri/mod.rs index 27d590a03..55c34c753 100644 --- a/core/src/custom_uri/mod.rs +++ b/core/src/custom_uri/mod.rs @@ -34,7 +34,7 @@ use mini_moka::sync::Cache; use sd_file_ext::text::is_text; use sd_p2p::{spaceblock::Range, spacetunnel::RemoteIdentity}; use tokio::{ - fs::File, + fs::{self, File}, io::{AsyncReadExt, AsyncSeekExt}, }; use tokio_util::sync::PollSender; @@ -202,8 +202,8 @@ pub fn router(node: Arc) -> Router<()> { match serve_from { ServeFrom::Local => { - let metadata = file_path_full_path - .metadata() + let metadata = fs::metadata(&file_path_full_path) + .await .map_err(internal_server_error)?; (!metadata.is_dir()) .then_some(()) @@ -286,6 +286,43 @@ pub fn router(node: Arc) -> Router<()> { }, ), ) + .route( + "/local-file-by-path/:path", + get( + |extract::Path(path): extract::Path, request: Request| async move { + let path = PathBuf::from(path); + + let metadata = fs::metadata(&path).await.map_err(internal_server_error)?; + (!metadata.is_dir()) + .then_some(()) + .ok_or_else(|| not_found(()))?; + + let mut file = File::open(&path).await.map_err(|err| { + InfallibleResponse::builder() + .status(if err.kind() == io::ErrorKind::NotFound { + StatusCode::NOT_FOUND + } else { + StatusCode::INTERNAL_SERVER_ERROR + }) + .body(body::boxed(Full::from(""))) + })?; + + let resp = InfallibleResponse::builder().header( + "Content-Type", + HeaderValue::from_str(&match path.extension().and_then(OsStr::to_str) { + None => "text/plain".to_string(), + Some(ext) => infer_the_mime_type(ext, &mut file, &metadata).await?, + }) + .map_err(|err| { + error!("Error converting mime-type into header value: {}", err); + internal_server_error(()) + })?, + ); + + serve_file(file, Ok(metadata), request.into_parts().0, resp).await + }, + ), + ) .route_layer(middleware::from_fn(cors_middleware)) .with_state({ let file_metadata_cache = Arc::new(Cache::new(150)); @@ -327,7 +364,8 @@ async fn infer_the_mime_type( file: &mut File, metadata: &Metadata, ) -> Result> { - let mime_type = match ext.to_lowercase().as_str() { + let ext = ext.to_lowercase(); + let mime_type = match ext.as_str() { // AAC audio "aac" => "audio/aac", // Musical Instrument Digital Interface (MIDI) @@ -416,7 +454,7 @@ async fn infer_the_mime_type( // Only browser recognized types, everything else should be text/plain // https://www.iana.org/assignments/media-types/media-types.xhtml#table-text - let mime_type = match ext { + let mime_type = match ext.as_str() { // HyperText Markup Language "html" | "htm" => "text/html", // Cascading Style Sheets diff --git a/core/src/location/file_path_helper/mod.rs b/core/src/location/file_path_helper/mod.rs index 125ff7b3c..4be0982f0 100644 --- a/core/src/location/file_path_helper/mod.rs +++ b/core/src/location/file_path_helper/mod.rs @@ -160,6 +160,8 @@ pub fn path_is_hidden(path: &Path, metadata: &Metadata) -> bool { const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2; + let _ = path; // just to avoid warnings on Windows + if (metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN { return true; } diff --git a/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx index 926777b08..d5d48f872 100644 --- a/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx +++ b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/Items.tsx @@ -4,16 +4,13 @@ import { ContextMenu, dialogManager, ModifierKeys, toast } from '@sd/ui'; import { Menu } from '~/components/Menu'; import { useKeybindFactory } from '~/hooks/useKeybindFactory'; import { isNonEmpty } from '~/util'; -import { usePlatform } from '~/util/Platform'; import { useExplorerContext } from '../../Context'; import { CopyAsPathBase } from '../../CopyAsPath'; import DeleteDialog from '../../FilePath/DeleteDialog'; import EraseDialog from '../../FilePath/EraseDialog'; -import { useViewItemDoubleClick } from '../../View/ViewItem'; -import { Conditional, ConditionalItem } from '../ConditionalItem'; +import { ConditionalItem } from '../ConditionalItem'; import { useContextMenuContext } from '../context'; -import OpenWith from './OpenWith'; export * from './CutCopyItems'; @@ -228,32 +225,3 @@ export const ParentFolderActions = new ConditionalItem({ ); } }); - -export const OpenOrDownload = new ConditionalItem({ - useCondition: () => { - const { selectedFilePaths } = useContextMenuContext(); - const { openFilePaths } = usePlatform(); - - if (!openFilePaths || !isNonEmpty(selectedFilePaths)) return null; - - return { openFilePaths, selectedFilePaths }; - }, - Component: () => { - const keybind = useKeybindFactory(); - const { platform } = usePlatform(); - const { doubleClick } = useViewItemDoubleClick(); - - if (platform === 'web') return ; - else - return ( - <> - doubleClick()} - /> - - - ); - } -}); diff --git a/interface/app/$libraryId/Explorer/ContextMenu/FilePath/OpenWith.tsx b/interface/app/$libraryId/Explorer/ContextMenu/FilePath/OpenWith.tsx deleted file mode 100644 index 766023a03..000000000 --- a/interface/app/$libraryId/Explorer/ContextMenu/FilePath/OpenWith.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { Suspense } from 'react'; -import { useLibraryContext } from '@sd/client'; -import { toast } from '@sd/ui'; -import { Menu } from '~/components/Menu'; -import { Platform, usePlatform } from '~/util/Platform'; - -import { ConditionalItem } from '../ConditionalItem'; -import { useContextMenuContext } from '../context'; - -export default new ConditionalItem({ - useCondition: () => { - const { selectedFilePaths } = useContextMenuContext(); - const { getFilePathOpenWithApps, openFilePathWith } = usePlatform(); - - if (!getFilePathOpenWithApps || !openFilePathWith) return null; - if (selectedFilePaths.some((p) => p.is_dir)) return null; - - return { getFilePathOpenWithApps, openFilePathWith }; - }, - Component: ({ getFilePathOpenWithApps, openFilePathWith }) => ( - - - - - - ) -}); - -const Items = ({ - actions -}: { - actions: Required>; -}) => { - const { selectedFilePaths } = useContextMenuContext(); - - const { library } = useLibraryContext(); - - const ids = selectedFilePaths.map((obj) => obj.id); - - const items = useQuery( - ['openWith', ids], - () => actions.getFilePathOpenWithApps(library.uuid, ids), - { suspense: true } - ); - - return ( - <> - {Array.isArray(items.data) && items.data.length > 0 ? ( - items.data.map((data, id) => ( - { - try { - await actions.openFilePathWith( - library.uuid, - ids.map((id) => [id, data.url]) - ); - } catch (e) { - toast.error(`Failed to open file, with: ${data.url}`); - } - }} - > - {data.name} - - )) - ) : ( -

No apps available

- )} - - ); -}; diff --git a/interface/app/$libraryId/Explorer/ContextMenu/OpenWith.tsx b/interface/app/$libraryId/Explorer/ContextMenu/OpenWith.tsx new file mode 100644 index 000000000..ca8b24bca --- /dev/null +++ b/interface/app/$libraryId/Explorer/ContextMenu/OpenWith.tsx @@ -0,0 +1,120 @@ +import { useQuery } from '@tanstack/react-query'; +import { Suspense } from 'react'; +import { useLibraryContext } from '@sd/client'; +import { toast } from '@sd/ui'; +import { Menu } from '~/components/Menu'; +import { Platform, usePlatform } from '~/util/Platform'; + +import { ConditionalItem } from './ConditionalItem'; +import { useContextMenuContext } from './context'; + +export default new ConditionalItem({ + useCondition: () => { + const { selectedFilePaths, selectedEphemeralPaths } = useContextMenuContext(); + const { + getFilePathOpenWithApps, + openFilePathWith, + getEphemeralFilesOpenWithApps, + openEphemeralFileWith + } = usePlatform(); + + if ( + !getFilePathOpenWithApps || + !openFilePathWith || + !getEphemeralFilesOpenWithApps || + !openEphemeralFileWith + ) + return null; + if (selectedFilePaths.some((p) => p.is_dir) || selectedEphemeralPaths.some((p) => p.is_dir)) + return null; + + return { + getFilePathOpenWithApps, + openFilePathWith, + getEphemeralFilesOpenWithApps, + openEphemeralFileWith + }; + }, + Component: ({ + getFilePathOpenWithApps, + openFilePathWith, + getEphemeralFilesOpenWithApps, + openEphemeralFileWith + }) => ( + + + + + + ) +}); + +const Items = ({ + actions +}: { + actions: Required< + Pick< + Platform, + | 'getFilePathOpenWithApps' + | 'openFilePathWith' + | 'getEphemeralFilesOpenWithApps' + | 'openEphemeralFileWith' + > + >; +}) => { + const { selectedFilePaths, selectedEphemeralPaths } = useContextMenuContext(); + + const { library } = useLibraryContext(); + + const ids = selectedFilePaths.map((obj) => obj.id); + const paths = selectedEphemeralPaths.map((obj) => obj.path); + + const items = useQuery( + ['openWith', ids, paths], + () => { + if (ids.length > 0) return actions.getFilePathOpenWithApps(library.uuid, ids); + else if (paths.length > 0) return actions.getEphemeralFilesOpenWithApps(paths); + else return { data: [] }; + }, + { suspense: true } + ); + + return ( + <> + {Array.isArray(items.data) && items.data.length > 0 ? ( + items.data.map((data, index) => ( + { + try { + if (ids.length > 0) { + await actions.openFilePathWith( + library.uuid, + ids.map((id) => [id, data.url]) + ); + } else if (paths.length > 0) { + await actions.openEphemeralFileWith( + paths.map((path) => [path, data.url]) + ); + } + } catch (e) { + toast.error(`Failed to open file, with: ${data.url}`); + } + }} + > + {data.name} + + )) + ) : ( +

No apps available

+ )} + + ); +}; diff --git a/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx b/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx index 602dba1ab..692f23bc7 100644 --- a/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx +++ b/interface/app/$libraryId/Explorer/ContextMenu/SharedItems.tsx @@ -1,18 +1,57 @@ +// tsc requires this import due to global types defined on it +import type {} from '@sd/client'; + import { FileX, Share as ShareIcon } from '@phosphor-icons/react'; import { useMemo } from 'react'; -import { ContextMenu, ModifierKeys } from '@sd/ui'; +import { ContextMenu, ModifierKeys, toast } from '@sd/ui'; import { Menu } from '~/components/Menu'; import { useKeybindFactory } from '~/hooks/useKeybindFactory'; import { isNonEmpty } from '~/util'; -import { type Platform } from '~/util/Platform'; +import { usePlatform, type Platform } from '~/util/Platform'; import { useExplorerContext } from '../Context'; import { getQuickPreviewStore } from '../QuickPreview/store'; import { RevealInNativeExplorerBase } from '../RevealInNativeExplorer'; import { getExplorerStore, useExplorerStore } from '../store'; +import { useViewItemDoubleClick } from '../View/ViewItem'; import { useExplorerViewContext } from '../ViewContext'; -import { ConditionalItem } from './ConditionalItem'; +import { Conditional, ConditionalItem } from './ConditionalItem'; import { useContextMenuContext } from './context'; +import OpenWith from './OpenWith'; + +export const OpenOrDownload = new ConditionalItem({ + useCondition: () => { + const { selectedFilePaths, selectedEphemeralPaths } = useContextMenuContext(); + const { openFilePaths, openEphemeralFiles } = usePlatform(); + + if ( + !openFilePaths || + !openEphemeralFiles || + (!isNonEmpty(selectedFilePaths) && !isNonEmpty(selectedEphemeralPaths)) + ) + return null; + + return { openFilePaths, openEphemeralFiles, selectedFilePaths, selectedEphemeralPaths }; + }, + Component: () => { + const keybind = useKeybindFactory(); + const { platform } = usePlatform(); + const { doubleClick } = useViewItemDoubleClick(); + + if (platform === 'web') return ; + else + return ( + <> + doubleClick()} + /> + + + ); + } +}); export const OpenQuickView = () => { const keybind = useKeybindFactory(); @@ -111,6 +150,14 @@ export const RevealInNativeExplorer = new ConditionalItem({ }); break; } + case 'NonIndexedPath': { + array.push({ + Ephemeral: { + path: item.item.path + } + }); + break; + } } } diff --git a/interface/app/$libraryId/Explorer/ContextMenu/context.tsx b/interface/app/$libraryId/Explorer/ContextMenu/context.tsx index 65741476e..9d8b5e39c 100644 --- a/interface/app/$libraryId/Explorer/ContextMenu/context.tsx +++ b/interface/app/$libraryId/Explorer/ContextMenu/context.tsx @@ -1,11 +1,20 @@ import { createContext, PropsWithChildren, useContext } from 'react'; -import { ExplorerItem, FilePath, Object, useItemsAsFilePaths, useItemsAsObjects } from '@sd/client'; +import { + ExplorerItem, + FilePath, + NonIndexedPathItem, + Object, + useItemsAsEphemeralPaths, + useItemsAsFilePaths, + useItemsAsObjects +} from '@sd/client'; import { NonEmptyArray } from '~/util'; const ContextMenuContext = createContext<{ selectedItems: NonEmptyArray; selectedFilePaths: FilePath[]; selectedObjects: Object[]; + selectedEphemeralPaths: NonIndexedPathItem[]; } | null>(null); export const ContextMenuContextProvider = ({ @@ -16,9 +25,12 @@ export const ContextMenuContextProvider = ({ }>) => { const selectedFilePaths = useItemsAsFilePaths(selectedItems); const selectedObjects = useItemsAsObjects(selectedItems); + const selectedEphemeralPaths = useItemsAsEphemeralPaths(selectedItems); return ( - + {children} ); diff --git a/interface/app/$libraryId/Explorer/ContextMenu/index.tsx b/interface/app/$libraryId/Explorer/ContextMenu/index.tsx index 0f305616c..8409dc766 100644 --- a/interface/app/$libraryId/Explorer/ContextMenu/index.tsx +++ b/interface/app/$libraryId/Explorer/ContextMenu/index.tsx @@ -17,7 +17,7 @@ export * as SharedItems from './SharedItems'; const Items = ({ children }: PropsWithChildren) => ( <> - + diff --git a/interface/app/$libraryId/Explorer/FilePath/Thumb.tsx b/interface/app/$libraryId/Explorer/FilePath/Thumb.tsx index 9e6c5214d..e1f8c33ed 100644 --- a/interface/app/$libraryId/Explorer/FilePath/Thumb.tsx +++ b/interface/app/$libraryId/Explorer/FilePath/Thumb.tsx @@ -102,13 +102,10 @@ export const FileThumb = memo((props: ThumbProps) => { switch (thumbType) { case ThumbType.Original: - if ( - locationId && - filePath && - 'id' in filePath && - (itemData.extension !== 'pdf' || pdfViewerEnabled()) - ) { - setSrc(platform.getFileUrl(library.uuid, locationId, filePath.id)); + if (filePath && (itemData.extension !== 'pdf' || pdfViewerEnabled())) { + if ('id' in filePath && locationId) + setSrc(platform.getFileUrl(library.uuid, locationId, filePath.id)); + else if ('path' in filePath) setSrc(platform.getFileUrlByPath(filePath.path)); } else { setThumbType(ThumbType.Thumbnail); } diff --git a/interface/app/$libraryId/Explorer/Inspector/index.tsx b/interface/app/$libraryId/Explorer/Inspector/index.tsx index 80c28044c..b5194f8d6 100644 --- a/interface/app/$libraryId/Explorer/Inspector/index.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/index.tsx @@ -28,10 +28,13 @@ import { useLocation } from 'react-router'; import { Link as NavLink } from 'react-router-dom'; import { byteSize, + FilePath, + FilePathWithObject, getExplorerItemData, - getItemFilePath, - getItemObject, + NonIndexedPathItem, + Object, ObjectKindEnum, + ObjectWithFilePaths, useBridgeQuery, useItemsAsObjects, useLibraryQuery, @@ -158,52 +161,67 @@ const Thumbnails = ({ items }: { items: ExplorerItem[] }) => { }; export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => { - const objectData = getItemObject(item); + let objectData: Object | ObjectWithFilePaths | null = null; + let filePathData: FilePath | FilePathWithObject | null = null; + let ephemeralPathData: NonIndexedPathItem | null = null; + + switch (item.type) { + case 'NonIndexedPath': { + ephemeralPathData = item.item; + break; + } + case 'Path': { + objectData = item.item.object; + filePathData = item.item; + break; + } + case 'Object': { + objectData = item.item; + filePathData = item.item.file_paths[0] ?? null; + break; + } + } + const readyToFetch = useIsFetchReady(item); - const isNonIndexed = item.type === 'NonIndexedPath'; const tags = useLibraryQuery(['tags.getForObject', objectData?.id ?? -1], { - enabled: !!objectData && readyToFetch + enabled: objectData != null && readyToFetch }); const { libraryId } = useZodRouteParams(LibraryIdParamsSchema); - const object = useLibraryQuery(['files.get', { id: objectData?.id ?? -1 }], { - enabled: !!objectData && readyToFetch + const queriedFullPath = useLibraryQuery(['files.getPath', filePathData?.id ?? -1], { + enabled: filePathData != null && readyToFetch }); - const filePath = useLibraryQuery(['files.getPath', objectData?.id ?? -1], { - enabled: !!objectData && readyToFetch - }); - - //Images are only supported currently - kind = 5 const filesMediaData = useLibraryQuery(['files.getMediaData', objectData?.id ?? -1], { - enabled: objectData?.kind === ObjectKindEnum.Image && !isNonIndexed && readyToFetch + enabled: objectData?.kind === ObjectKindEnum.Image && readyToFetch }); const ephemeralLocationMediaData = useBridgeQuery( - ['files.getEphemeralMediaData', isNonIndexed ? item.item.path : ''], + ['files.getEphemeralMediaData', ephemeralPathData != null ? ephemeralPathData.path : ''], { - enabled: isNonIndexed && item.item.kind === 5 && readyToFetch + enabled: ephemeralPathData?.kind === ObjectKindEnum.Image && readyToFetch } ); const mediaData = filesMediaData ?? ephemeralLocationMediaData ?? null; - if (filePath.data == null && item.type === 'NonIndexedPath') { - filePath.data = item.item.path; - } + const fullPath = queriedFullPath.data ?? ephemeralPathData?.path; const { name, isDir, kind, size, casId, dateCreated, dateAccessed, dateModified, dateIndexed } = useExplorerItemData(item); - const pubId = object?.data ? uniqueId(object?.data) : null; + const pubId = objectData != null ? uniqueId(objectData) : null; - const filePathItem = getItemFilePath(item); let extension, integrityChecksum; - if (filePathItem) { - extension = filePathItem.extension; + if (filePathData != null) { + extension = filePathData.extension; integrityChecksum = - 'integrity_checksum' in filePathItem ? filePathItem.integrity_checksum : null; + 'integrity_checksum' in filePathData ? filePathData.integrity_checksum : null; + } + + if (ephemeralPathData != null) { + extension = ephemeralPathData.extension; } return ( @@ -241,21 +259,21 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => { - {isNonIndexed || ( + {ephemeralPathData != null || ( )} - {isNonIndexed || ( + {ephemeralPathData != null || ( )} { // TODO: Add toast notification - filePath.data && navigator.clipboard.writeText(filePath.data); + fullPath && navigator.clipboard.writeText(fullPath); }} /> @@ -299,9 +317,7 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => { - {isNonIndexed || ( - - )} + {integrityChecksum && ( { /> )} - {isNonIndexed || } + )} diff --git a/interface/app/$libraryId/Explorer/QuickPreview/index.tsx b/interface/app/$libraryId/Explorer/QuickPreview/index.tsx index 2db7088e0..9d966ba58 100644 --- a/interface/app/$libraryId/Explorer/QuickPreview/index.tsx +++ b/interface/app/$libraryId/Explorer/QuickPreview/index.tsx @@ -56,7 +56,7 @@ export const QuickPreview = () => { const rspc = useRspcLibraryContext(); const isDark = useIsDark(); const { library } = useLibraryContext(); - const { openFilePaths, revealItems } = usePlatform(); + const { openFilePaths, revealItems, openEphemeralFiles } = usePlatform(); const explorer = useExplorerContext(); const { open, itemIndex } = useQuickPreviewStore(); @@ -117,14 +117,18 @@ export const QuickPreview = () => { // Open file useKeybind([os === 'macOS' ? ModifierKeys.Meta : ModifierKeys.Control, 'o'], () => { - if (!item || !openFilePaths) return; + if (!item || !openFilePaths || !openEphemeralFiles) return; try { - const path = getIndexedItemFilePath(item); + if (item.type === 'Path' || item.type === 'Object') { + const path = getIndexedItemFilePath(item); - if (!path) throw 'No path found'; + if (!path) throw 'No path found'; - openFilePaths(library.uuid, [path.id]); + openFilePaths(library.uuid, [path.id]); + } else if (item.type === 'NonIndexedPath') { + openEphemeralFiles([item.item.path]); + } } catch (error) { toast.error({ title: 'Failed to open file', @@ -138,13 +142,18 @@ export const QuickPreview = () => { if (!item || !revealItems) return; try { - const id = item.type === 'Location' ? item.item.id : getIndexedItemFilePath(item)?.id; + const toReveal = []; + if (item.type === 'Location') { + toReveal.push({ Location: { id: item.item.id } }); + } else if (item.type === 'NonIndexedPath') { + toReveal.push({ Ephemeral: { path: item.item.path } }); + } else { + const filePath = getIndexedItemFilePath(item); + if (!filePath) throw 'No file path found'; + toReveal.push({ FilePath: { id: filePath.id } }); + } - if (!id) throw 'No id found'; - - revealItems(library.uuid, [ - { ...(item.type === 'Location' ? { Location: { id } } : { FilePath: { id } }) } - ]); + revealItems(library.uuid, toReveal); } catch (error) { toast.error({ title: 'Failed to reveal', @@ -313,66 +322,63 @@ export const QuickPreview = () => {
- {item.type !== 'NonIndexedPath' && ( - - - - - - -
- } - onOpenChange={setIsContextMenuOpen} - align="end" - sideOffset={-10} - > - - + + + + + + + + } + onOpenChange={setIsContextMenuOpen} + align="end" + sideOffset={-10} + > + + + {item.type !== 'NonIndexedPath' && ( name && setIsRenaming(true)} /> + )} - + - - {(items) => ( - - {items} - - )} - + + {(items) => ( + + {items} + + )} + - - - - )} + + + { const navigate = useNavigate(); const explorer = useExplorerContext(); const { library } = useLibraryContext(); - const { openFilePaths } = usePlatform(); + const { openFilePaths, openEphemeralFiles } = usePlatform(); const updateAccessTime = useLibraryMutation('files.updateAccessTime'); @@ -127,12 +127,28 @@ export const useViewItemDoubleClick = () => { } if (items.non_indexed.length > 0) { - const [non_indexed] = items.non_indexed; - if (non_indexed) { - navigate({ - search: createSearchParams({ path: non_indexed.path }).toString() - }); - return; + if (items.non_indexed.length === 1) { + const [non_indexed] = items.non_indexed; + if (non_indexed && non_indexed.is_dir) { + navigate({ + search: createSearchParams({ path: non_indexed.path }).toString() + }); + return; + } + } + + if (explorer.settingsStore.openOnDoubleClick === 'openFile' && openEphemeralFiles) { + try { + await openEphemeralFiles(items.non_indexed.map(({ path }) => path)); + } catch (error) { + toast.error({ title: 'Failed to open file', body: `Error: ${error}.` }); + } + } else if (item && explorer.settingsStore.openOnDoubleClick === 'quickPreview') { + if (item.type !== 'Location' && !(isPath(item) && item.item.is_dir)) { + getQuickPreviewStore().itemIndex = itemIndex; + getQuickPreviewStore().open = true; + return; + } } } }, @@ -142,6 +158,7 @@ export const useViewItemDoubleClick = () => { library.uuid, navigate, openFilePaths, + openEphemeralFiles, updateAccessTime ] ); diff --git a/interface/app/$libraryId/Explorer/View/index.tsx b/interface/app/$libraryId/Explorer/View/index.tsx index cb187bcbc..b861b34b4 100644 --- a/interface/app/$libraryId/Explorer/View/index.tsx +++ b/interface/app/$libraryId/Explorer/View/index.tsx @@ -179,7 +179,7 @@ export const EmptyNotice = (props: { if (props.loading) return null; return ( -
+
{props.icon ? isValidElement(props.icon) ? props.icon @@ -198,7 +198,7 @@ const useKeyDownHandlers = ({ disabled }: { disabled: boolean }) => { const os = useOperatingSystem(); const { library } = useLibraryContext(); - const { openFilePaths } = usePlatform(); + const { openFilePaths, openEphemeralFiles } = usePlatform(); const handleNewTag = useCallback( async (event: KeyboardEvent) => { diff --git a/interface/components/TextViewer/one-dark.scss b/interface/components/TextViewer/one-dark.scss index 037e40a0e..268c71a3d 100644 --- a/interface/components/TextViewer/one-dark.scss +++ b/interface/components/TextViewer/one-dark.scss @@ -5,7 +5,7 @@ * Based on Atom's One Dark theme: https://github.com/atom/atom/tree/master/packages/one-dark-syntax */ - /** +/** * One Dark colours (accurate as of commit 8ae45ca on 6 Sep 2018) * From colors.less * --mono-1: hsl(220, 14%, 71%); @@ -30,413 +30,416 @@ * --syntax-cursor-line: hsla(220, 100%, 80%, 0.04); */ - code[class*="language-"], - pre[class*="language-"] { - background: hsl(220, 13%, 18%); - color: hsl(220, 14%, 71%); - text-shadow: 0 1px rgba(0, 0, 0, 0.3); - font-family: "Fira Code", "Fira Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace; - direction: ltr; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - line-height: 1.5; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; - } +code[class*='language-'], +pre[class*='language-'] { + background: hsl(220, 13%, 18%); + color: hsl(220, 14%, 71%); + text-shadow: 0 1px rgba(0, 0, 0, 0.3); + font-family: 'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + line-height: 1.5; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} - /* Selection */ - code[class*="language-"]::-moz-selection, - code[class*="language-"] *::-moz-selection, - pre[class*="language-"] *::-moz-selection { - background: hsl(220, 13%, 28%); - color: inherit; +/* Selection */ +code[class*='language-']::-moz-selection, +code[class*='language-'] *::-moz-selection, +pre[class*='language-'] *::-moz-selection { + background: hsl(220, 13%, 28%); + color: inherit; + text-shadow: none; +} + +code[class*='language-']::selection, +code[class*='language-'] *::selection, +pre[class*='language-'] *::selection { + background: hsl(220, 13%, 28%); + color: inherit; + text-shadow: none; +} + +/* Code blocks */ +pre[class*='language-'] { + padding: 1em; + margin: 0.5em 0; + overflow: auto; + border-radius: 0.3em; +} + +/* Inline code */ +:not(pre) > code[class*='language-'] { + padding: 0.2em 0.3em; + border-radius: 0.3em; + white-space: normal; +} + +/* Print */ +@media print { + code[class*='language-'], + pre[class*='language-'] { text-shadow: none; } +} - code[class*="language-"]::selection, - code[class*="language-"] *::selection, - pre[class*="language-"] *::selection { - background: hsl(220, 13%, 28%); - color: inherit; - text-shadow: none; - } +.token.comment, +.token.prolog, +.token.cdata { + color: hsl(220, 10%, 40%); +} - /* Code blocks */ - pre[class*="language-"] { - padding: 1em; - margin: 0.5em 0; - overflow: auto; - border-radius: 0.3em; - } +.token.doctype, +.token.punctuation, +.token.entity { + color: hsl(220, 14%, 71%); +} - /* Inline code */ - :not(pre) > code[class*="language-"] { - padding: 0.2em 0.3em; - border-radius: 0.3em; - white-space: normal; - } +.token.attr-name, +.token.class-name, +.token.boolean, +.token.constant, +.token.number, +.token.atrule { + color: hsl(29, 54%, 61%); +} - /* Print */ - @media print { - code[class*="language-"], - pre[class*="language-"] { - text-shadow: none; - } - } +.token.keyword { + color: hsl(286, 60%, 67%); +} - .token.comment, - .token.prolog, - .token.cdata { - color: hsl(220, 10%, 40%); - } +.token.property, +.token.tag, +.token.symbol, +.token.deleted, +.token.important { + color: hsl(355, 65%, 65%); +} - .token.doctype, - .token.punctuation, - .token.entity { - color: hsl(220, 14%, 71%); - } +.token.selector, +.token.string, +.token.char, +.token.builtin, +.token.inserted, +.token.regex, +.token.attr-value, +.token.attr-value > .token.punctuation { + color: hsl(95, 38%, 62%); +} - .token.attr-name, - .token.class-name, - .token.boolean, - .token.constant, - .token.number, - .token.atrule { - color: hsl(29, 54%, 61%); - } +.token.variable, +.token.operator, +.token.function { + color: hsl(207, 82%, 66%); +} - .token.keyword { - color: hsl(286, 60%, 67%); - } +.token.url { + color: hsl(187, 47%, 55%); +} - .token.property, - .token.tag, - .token.symbol, - .token.deleted, - .token.important { - color: hsl(355, 65%, 65%); - } +/* HTML overrides */ +.token.attr-value > .token.punctuation.attr-equals, +.token.special-attr > .token.attr-value > .token.value.css { + color: hsl(220, 14%, 71%); +} - .token.selector, - .token.string, - .token.char, - .token.builtin, - .token.inserted, - .token.regex, - .token.attr-value, - .token.attr-value > .token.punctuation { - color: hsl(95, 38%, 62%); - } +/* CSS overrides */ +.language-css .token.selector { + color: hsl(355, 65%, 65%); +} - .token.variable, - .token.operator, - .token.function { - color: hsl(207, 82%, 66%); - } +.language-css .token.property { + color: hsl(220, 14%, 71%); +} - .token.url { - color: hsl(187, 47%, 55%); - } +.language-css .token.function, +.language-css .token.url > .token.function { + color: hsl(187, 47%, 55%); +} - /* HTML overrides */ - .token.attr-value > .token.punctuation.attr-equals, - .token.special-attr > .token.attr-value > .token.value.css { - color: hsl(220, 14%, 71%); - } +.language-css .token.url > .token.string.url { + color: hsl(95, 38%, 62%); +} - /* CSS overrides */ - .language-css .token.selector { - color: hsl(355, 65%, 65%); - } +.language-css .token.important, +.language-css .token.atrule .token.rule { + color: hsl(286, 60%, 67%); +} - .language-css .token.property { - color: hsl(220, 14%, 71%); - } +/* JS overrides */ +.language-javascript .token.operator { + color: hsl(286, 60%, 67%); +} - .language-css .token.function, - .language-css .token.url > .token.function { - color: hsl(187, 47%, 55%); - } +.language-javascript + .token.template-string + > .token.interpolation + > .token.interpolation-punctuation.punctuation { + color: hsl(5, 48%, 51%); +} - .language-css .token.url > .token.string.url { - color: hsl(95, 38%, 62%); - } +/* JSON overrides */ +.language-json .token.operator { + color: hsl(220, 14%, 71%); +} - .language-css .token.important, - .language-css .token.atrule .token.rule { - color: hsl(286, 60%, 67%); - } +.language-json .token.null.keyword { + color: hsl(29, 54%, 61%); +} - /* JS overrides */ - .language-javascript .token.operator { - color: hsl(286, 60%, 67%); - } +/* MD overrides */ +.language-markdown .token.url, +.language-markdown .token.url > .token.operator, +.language-markdown .token.url-reference.url > .token.string { + color: hsl(220, 14%, 71%); +} - .language-javascript .token.template-string > .token.interpolation > .token.interpolation-punctuation.punctuation { - color: hsl(5, 48%, 51%); - } +.language-markdown .token.url > .token.content { + color: hsl(207, 82%, 66%); +} - /* JSON overrides */ - .language-json .token.operator { - color: hsl(220, 14%, 71%); - } +.language-markdown .token.url > .token.url, +.language-markdown .token.url-reference.url { + color: hsl(187, 47%, 55%); +} - .language-json .token.null.keyword { - color: hsl(29, 54%, 61%); - } +.language-markdown .token.blockquote.punctuation, +.language-markdown .token.hr.punctuation { + color: hsl(220, 10%, 40%); + font-style: italic; +} - /* MD overrides */ - .language-markdown .token.url, - .language-markdown .token.url > .token.operator, - .language-markdown .token.url-reference.url > .token.string { - color: hsl(220, 14%, 71%); - } +.language-markdown .token.code-snippet { + color: hsl(95, 38%, 62%); +} - .language-markdown .token.url > .token.content { - color: hsl(207, 82%, 66%); - } +.language-markdown .token.bold .token.content { + color: hsl(29, 54%, 61%); +} - .language-markdown .token.url > .token.url, - .language-markdown .token.url-reference.url { - color: hsl(187, 47%, 55%); - } +.language-markdown .token.italic .token.content { + color: hsl(286, 60%, 67%); +} - .language-markdown .token.blockquote.punctuation, - .language-markdown .token.hr.punctuation { - color: hsl(220, 10%, 40%); - font-style: italic; - } +.language-markdown .token.strike .token.content, +.language-markdown .token.strike .token.punctuation, +.language-markdown .token.list.punctuation, +.language-markdown .token.title.important > .token.punctuation { + color: hsl(355, 65%, 65%); +} - .language-markdown .token.code-snippet { - color: hsl(95, 38%, 62%); - } +/* General */ +.token.bold { + font-weight: bold; +} - .language-markdown .token.bold .token.content { - color: hsl(29, 54%, 61%); - } +.token.comment, +.token.italic { + font-style: italic; +} - .language-markdown .token.italic .token.content { - color: hsl(286, 60%, 67%); - } +.token.entity { + cursor: help; +} - .language-markdown .token.strike .token.content, - .language-markdown .token.strike .token.punctuation, - .language-markdown .token.list.punctuation, - .language-markdown .token.title.important > .token.punctuation { - color: hsl(355, 65%, 65%); - } +.token.namespace { + opacity: 0.8; +} - /* General */ - .token.bold { - font-weight: bold; - } +/* Plugin overrides */ +/* Selectors should have higher specificity than those in the plugins' default stylesheets */ - .token.comment, - .token.italic { - font-style: italic; - } +/* Show Invisibles plugin overrides */ +.token.token.tab:not(:empty):before, +.token.token.cr:before, +.token.token.lf:before, +.token.token.space:before { + color: hsla(220, 14%, 71%, 0.15); + text-shadow: none; +} - .token.entity { - cursor: help; - } +/* Toolbar plugin overrides */ +/* Space out all buttons and move them away from the right edge of the code block */ +div.code-toolbar > .toolbar.toolbar > .toolbar-item { + margin-right: 0.4em; +} - .token.namespace { - opacity: 0.8; - } +/* Styling the buttons */ +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span { + background: hsl(220, 13%, 26%); + color: hsl(220, 9%, 55%); + padding: 0.1em 0.4em; + border-radius: 0.3em; +} - /* Plugin overrides */ - /* Selectors should have higher specificity than those in the plugins' default stylesheets */ +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus { + background: hsl(220, 13%, 28%); + color: hsl(220, 14%, 71%); +} - /* Show Invisibles plugin overrides */ - .token.token.tab:not(:empty):before, - .token.token.cr:before, - .token.token.lf:before, - .token.token.space:before { - color: hsla(220, 14%, 71%, 0.15); - text-shadow: none; - } +/* Line Highlight plugin overrides */ +/* The highlighted line itself */ +.line-highlight.line-highlight { + background: hsla(220, 100%, 80%, 0.04); +} - /* Toolbar plugin overrides */ - /* Space out all buttons and move them away from the right edge of the code block */ - div.code-toolbar > .toolbar.toolbar > .toolbar-item { - margin-right: 0.4em; - } +/* Default line numbers in Line Highlight plugin */ +.line-highlight.line-highlight:before, +.line-highlight.line-highlight[data-end]:after { + background: hsl(220, 13%, 26%); + color: hsl(220, 14%, 71%); + padding: 0.1em 0.6em; + border-radius: 0.3em; + box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); /* same as Toolbar plugin default */ +} - /* Styling the buttons */ - div.code-toolbar > .toolbar.toolbar > .toolbar-item > button, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > a, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > span { - background: hsl(220, 13%, 26%); - color: hsl(220, 9%, 55%); - padding: 0.1em 0.4em; - border-radius: 0.3em; - } +/* Hovering over a linkable line number (in the gutter area) */ +/* Requires Line Numbers plugin as well */ +pre[id].linkable-line-numbers.linkable-line-numbers span.line-numbers-rows > span:hover:before { + background-color: hsla(220, 100%, 80%, 0.04); +} - div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus { - background: hsl(220, 13%, 28%); - color: hsl(220, 14%, 71%); - } +/* Line Numbers and Command Line plugins overrides */ +/* Line separating gutter from coding area */ +.line-numbers.line-numbers .line-numbers-rows, +.command-line .command-line-prompt { + border-right-color: hsla(220, 14%, 71%, 0.15); +} - /* Line Highlight plugin overrides */ - /* The highlighted line itself */ - .line-highlight.line-highlight { - background: hsla(220, 100%, 80%, 0.04); - } +/* Stuff in the gutter */ +.line-numbers .line-numbers-rows > span:before, +.command-line .command-line-prompt > span:before { + color: hsl(220, 14%, 45%); +} - /* Default line numbers in Line Highlight plugin */ - .line-highlight.line-highlight:before, - .line-highlight.line-highlight[data-end]:after { - background: hsl(220, 13%, 26%); - color: hsl(220, 14%, 71%); - padding: 0.1em 0.6em; - border-radius: 0.3em; - box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); /* same as Toolbar plugin default */ - } +/* Match Braces plugin overrides */ +/* Note: Outline colour is inherited from the braces */ +.rainbow-braces .token.token.punctuation.brace-level-1, +.rainbow-braces .token.token.punctuation.brace-level-5, +.rainbow-braces .token.token.punctuation.brace-level-9 { + color: hsl(355, 65%, 65%); +} - /* Hovering over a linkable line number (in the gutter area) */ - /* Requires Line Numbers plugin as well */ - pre[id].linkable-line-numbers.linkable-line-numbers span.line-numbers-rows > span:hover:before { - background-color: hsla(220, 100%, 80%, 0.04); - } +.rainbow-braces .token.token.punctuation.brace-level-2, +.rainbow-braces .token.token.punctuation.brace-level-6, +.rainbow-braces .token.token.punctuation.brace-level-10 { + color: hsl(95, 38%, 62%); +} - /* Line Numbers and Command Line plugins overrides */ - /* Line separating gutter from coding area */ - .line-numbers.line-numbers .line-numbers-rows, - .command-line .command-line-prompt { - border-right-color: hsla(220, 14%, 71%, 0.15); - } +.rainbow-braces .token.token.punctuation.brace-level-3, +.rainbow-braces .token.token.punctuation.brace-level-7, +.rainbow-braces .token.token.punctuation.brace-level-11 { + color: hsl(207, 82%, 66%); +} - /* Stuff in the gutter */ - .line-numbers .line-numbers-rows > span:before, - .command-line .command-line-prompt > span:before { - color: hsl(220, 14%, 45%); - } +.rainbow-braces .token.token.punctuation.brace-level-4, +.rainbow-braces .token.token.punctuation.brace-level-8, +.rainbow-braces .token.token.punctuation.brace-level-12 { + color: hsl(286, 60%, 67%); +} - /* Match Braces plugin overrides */ - /* Note: Outline colour is inherited from the braces */ - .rainbow-braces .token.token.punctuation.brace-level-1, - .rainbow-braces .token.token.punctuation.brace-level-5, - .rainbow-braces .token.token.punctuation.brace-level-9 { - color: hsl(355, 65%, 65%); - } +/* Diff Highlight plugin overrides */ +/* Taken from https://github.com/atom/github/blob/master/styles/variables.less */ +pre.diff-highlight > code .token.token.deleted:not(.prefix), +pre > code.diff-highlight .token.token.deleted:not(.prefix) { + background-color: hsla(353, 100%, 66%, 0.15); +} - .rainbow-braces .token.token.punctuation.brace-level-2, - .rainbow-braces .token.token.punctuation.brace-level-6, - .rainbow-braces .token.token.punctuation.brace-level-10 { - color: hsl(95, 38%, 62%); - } +pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection, +pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix) *::-moz-selection { + background-color: hsla(353, 95%, 66%, 0.25); +} - .rainbow-braces .token.token.punctuation.brace-level-3, - .rainbow-braces .token.token.punctuation.brace-level-7, - .rainbow-braces .token.token.punctuation.brace-level-11 { - color: hsl(207, 82%, 66%); - } +pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection, +pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection { + background-color: hsla(353, 95%, 66%, 0.25); +} - .rainbow-braces .token.token.punctuation.brace-level-4, - .rainbow-braces .token.token.punctuation.brace-level-8, - .rainbow-braces .token.token.punctuation.brace-level-12 { - color: hsl(286, 60%, 67%); - } +pre.diff-highlight > code .token.token.inserted:not(.prefix), +pre > code.diff-highlight .token.token.inserted:not(.prefix) { + background-color: hsla(137, 100%, 55%, 0.15); +} - /* Diff Highlight plugin overrides */ - /* Taken from https://github.com/atom/github/blob/master/styles/variables.less */ - pre.diff-highlight > code .token.token.deleted:not(.prefix), - pre > code.diff-highlight .token.token.deleted:not(.prefix) { - background-color: hsla(353, 100%, 66%, 0.15); - } +pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection, +pre.diff-highlight > code .token.token.inserted:not(.prefix) *::-moz-selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix) *::-moz-selection { + background-color: hsla(135, 73%, 55%, 0.25); +} - pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection, - pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection, - pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection, - pre > code.diff-highlight .token.token.deleted:not(.prefix) *::-moz-selection { - background-color: hsla(353, 95%, 66%, 0.25); - } +pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection, +pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection { + background-color: hsla(135, 73%, 55%, 0.25); +} - pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection, - pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection, - pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection, - pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection { - background-color: hsla(353, 95%, 66%, 0.25); - } +/* Previewers plugin overrides */ +/* Based on https://github.com/atom-community/atom-ide-datatip/blob/master/styles/atom-ide-datatips.less and https://github.com/atom/atom/blob/master/packages/one-dark-ui */ +/* Border around popup */ +.prism-previewer.prism-previewer:before, +.prism-previewer-gradient.prism-previewer-gradient div { + border-color: hsl(224, 13%, 17%); +} - pre.diff-highlight > code .token.token.inserted:not(.prefix), - pre > code.diff-highlight .token.token.inserted:not(.prefix) { - background-color: hsla(137, 100%, 55%, 0.15); - } +/* Angle and time should remain as circles and are hence not included */ +.prism-previewer-color.prism-previewer-color:before, +.prism-previewer-gradient.prism-previewer-gradient div, +.prism-previewer-easing.prism-previewer-easing:before { + border-radius: 0.3em; +} - pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection, - pre.diff-highlight > code .token.token.inserted:not(.prefix) *::-moz-selection, - pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection, - pre > code.diff-highlight .token.token.inserted:not(.prefix) *::-moz-selection { - background-color: hsla(135, 73%, 55%, 0.25); - } +/* Triangles pointing to the code */ +.prism-previewer.prism-previewer:after { + border-top-color: hsl(224, 13%, 17%); +} - pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection, - pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection, - pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection, - pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection { - background-color: hsla(135, 73%, 55%, 0.25); - } +.prism-previewer-flipped.prism-previewer-flipped.after { + border-bottom-color: hsl(224, 13%, 17%); +} - /* Previewers plugin overrides */ - /* Based on https://github.com/atom-community/atom-ide-datatip/blob/master/styles/atom-ide-datatips.less and https://github.com/atom/atom/blob/master/packages/one-dark-ui */ - /* Border around popup */ - .prism-previewer.prism-previewer:before, - .prism-previewer-gradient.prism-previewer-gradient div { - border-color: hsl(224, 13%, 17%); - } +/* Background colour within the popup */ +.prism-previewer-angle.prism-previewer-angle:before, +.prism-previewer-time.prism-previewer-time:before, +.prism-previewer-easing.prism-previewer-easing { + background: hsl(219, 13%, 22%); +} - /* Angle and time should remain as circles and are hence not included */ - .prism-previewer-color.prism-previewer-color:before, - .prism-previewer-gradient.prism-previewer-gradient div, - .prism-previewer-easing.prism-previewer-easing:before { - border-radius: 0.3em; - } +/* For angle, this is the positive area (eg. 90deg will display one quadrant in this colour) */ +/* For time, this is the alternate colour */ +.prism-previewer-angle.prism-previewer-angle circle, +.prism-previewer-time.prism-previewer-time circle { + stroke: hsl(220, 14%, 71%); + stroke-opacity: 1; +} - /* Triangles pointing to the code */ - .prism-previewer.prism-previewer:after { - border-top-color: hsl(224, 13%, 17%); - } +/* Stroke colours of the handle, direction point, and vector itself */ +.prism-previewer-easing.prism-previewer-easing circle, +.prism-previewer-easing.prism-previewer-easing path, +.prism-previewer-easing.prism-previewer-easing line { + stroke: hsl(220, 14%, 71%); +} - .prism-previewer-flipped.prism-previewer-flipped.after { - border-bottom-color: hsl(224, 13%, 17%); - } - - /* Background colour within the popup */ - .prism-previewer-angle.prism-previewer-angle:before, - .prism-previewer-time.prism-previewer-time:before, - .prism-previewer-easing.prism-previewer-easing { - background: hsl(219, 13%, 22%); - } - - /* For angle, this is the positive area (eg. 90deg will display one quadrant in this colour) */ - /* For time, this is the alternate colour */ - .prism-previewer-angle.prism-previewer-angle circle, - .prism-previewer-time.prism-previewer-time circle { - stroke: hsl(220, 14%, 71%); - stroke-opacity: 1; - } - - /* Stroke colours of the handle, direction point, and vector itself */ - .prism-previewer-easing.prism-previewer-easing circle, - .prism-previewer-easing.prism-previewer-easing path, - .prism-previewer-easing.prism-previewer-easing line { - stroke: hsl(220, 14%, 71%); - } - - /* Fill colour of the handle */ - .prism-previewer-easing.prism-previewer-easing circle { - fill: transparent; - } +/* Fill colour of the handle */ +.prism-previewer-easing.prism-previewer-easing circle { + fill: transparent; +} diff --git a/interface/components/TextViewer/one-light.scss b/interface/components/TextViewer/one-light.scss index 9438ebf7a..822ab4eec 100644 --- a/interface/components/TextViewer/one-light.scss +++ b/interface/components/TextViewer/one-light.scss @@ -5,7 +5,7 @@ * Based on Atom's One Light theme: https://github.com/atom/atom/tree/master/packages/one-light-syntax */ - /** +/** * One Light colours (accurate as of commit eb064bf on 19 Feb 2021) * From colors.less * --mono-1: hsl(230, 8%, 24%); @@ -30,401 +30,404 @@ * --syntax-cursor-line: hsla(230, 8%, 24%, 0.05); */ - code[class*="language-"], - pre[class*="language-"] { - background: hsl(230, 1%, 98%); - color: hsl(230, 8%, 24%); - font-family: "Fira Code", "Fira Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace; - direction: ltr; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - line-height: 1.5; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; - } +code[class*='language-'], +pre[class*='language-'] { + background: hsl(230, 1%, 98%); + color: hsl(230, 8%, 24%); + font-family: 'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + line-height: 1.5; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} - /* Selection */ - code[class*="language-"]::-moz-selection, - code[class*="language-"] *::-moz-selection, - pre[class*="language-"] *::-moz-selection { - background: hsl(230, 1%, 90%); - color: inherit; - } +/* Selection */ +code[class*='language-']::-moz-selection, +code[class*='language-'] *::-moz-selection, +pre[class*='language-'] *::-moz-selection { + background: hsl(230, 1%, 90%); + color: inherit; +} - code[class*="language-"]::selection, - code[class*="language-"] *::selection, - pre[class*="language-"] *::selection { - background: hsl(230, 1%, 90%); - color: inherit; - } +code[class*='language-']::selection, +code[class*='language-'] *::selection, +pre[class*='language-'] *::selection { + background: hsl(230, 1%, 90%); + color: inherit; +} - /* Code blocks */ - pre[class*="language-"] { - padding: 1em; - margin: 0.5em 0; - overflow: auto; - border-radius: 0.3em; - } +/* Code blocks */ +pre[class*='language-'] { + padding: 1em; + margin: 0.5em 0; + overflow: auto; + border-radius: 0.3em; +} - /* Inline code */ - :not(pre) > code[class*="language-"] { - padding: 0.2em 0.3em; - border-radius: 0.3em; - white-space: normal; - } +/* Inline code */ +:not(pre) > code[class*='language-'] { + padding: 0.2em 0.3em; + border-radius: 0.3em; + white-space: normal; +} - .token.comment, - .token.prolog, - .token.cdata { - color: hsl(230, 4%, 64%); - } +.token.comment, +.token.prolog, +.token.cdata { + color: hsl(230, 4%, 64%); +} - .token.doctype, - .token.punctuation, - .token.entity { - color: hsl(230, 8%, 24%); - } +.token.doctype, +.token.punctuation, +.token.entity { + color: hsl(230, 8%, 24%); +} - .token.attr-name, - .token.class-name, - .token.boolean, - .token.constant, - .token.number, - .token.atrule { - color: hsl(35, 99%, 36%); - } +.token.attr-name, +.token.class-name, +.token.boolean, +.token.constant, +.token.number, +.token.atrule { + color: hsl(35, 99%, 36%); +} - .token.keyword { - color: hsl(301, 63%, 40%); - } +.token.keyword { + color: hsl(301, 63%, 40%); +} - .token.property, - .token.tag, - .token.symbol, - .token.deleted, - .token.important { - color: hsl(5, 74%, 59%); - } +.token.property, +.token.tag, +.token.symbol, +.token.deleted, +.token.important { + color: hsl(5, 74%, 59%); +} - .token.selector, - .token.string, - .token.char, - .token.builtin, - .token.inserted, - .token.regex, - .token.attr-value, - .token.attr-value > .token.punctuation { - color: hsl(119, 34%, 47%); - } +.token.selector, +.token.string, +.token.char, +.token.builtin, +.token.inserted, +.token.regex, +.token.attr-value, +.token.attr-value > .token.punctuation { + color: hsl(119, 34%, 47%); +} - .token.variable, - .token.operator, - .token.function { - color: hsl(221, 87%, 60%); - } +.token.variable, +.token.operator, +.token.function { + color: hsl(221, 87%, 60%); +} - .token.url { - color: hsl(198, 99%, 37%); - } +.token.url { + color: hsl(198, 99%, 37%); +} - /* HTML overrides */ - .token.attr-value > .token.punctuation.attr-equals, - .token.special-attr > .token.attr-value > .token.value.css { - color: hsl(230, 8%, 24%); - } +/* HTML overrides */ +.token.attr-value > .token.punctuation.attr-equals, +.token.special-attr > .token.attr-value > .token.value.css { + color: hsl(230, 8%, 24%); +} - /* CSS overrides */ - .language-css .token.selector { - color: hsl(5, 74%, 59%); - } +/* CSS overrides */ +.language-css .token.selector { + color: hsl(5, 74%, 59%); +} - .language-css .token.property { - color: hsl(230, 8%, 24%); - } +.language-css .token.property { + color: hsl(230, 8%, 24%); +} - .language-css .token.function, - .language-css .token.url > .token.function { - color: hsl(198, 99%, 37%); - } +.language-css .token.function, +.language-css .token.url > .token.function { + color: hsl(198, 99%, 37%); +} - .language-css .token.url > .token.string.url { - color: hsl(119, 34%, 47%); - } +.language-css .token.url > .token.string.url { + color: hsl(119, 34%, 47%); +} - .language-css .token.important, - .language-css .token.atrule .token.rule { - color: hsl(301, 63%, 40%); - } +.language-css .token.important, +.language-css .token.atrule .token.rule { + color: hsl(301, 63%, 40%); +} - /* JS overrides */ - .language-javascript .token.operator { - color: hsl(301, 63%, 40%); - } +/* JS overrides */ +.language-javascript .token.operator { + color: hsl(301, 63%, 40%); +} - .language-javascript .token.template-string > .token.interpolation > .token.interpolation-punctuation.punctuation { - color: hsl(344, 84%, 43%); - } +.language-javascript + .token.template-string + > .token.interpolation + > .token.interpolation-punctuation.punctuation { + color: hsl(344, 84%, 43%); +} - /* JSON overrides */ - .language-json .token.operator { - color: hsl(230, 8%, 24%); - } +/* JSON overrides */ +.language-json .token.operator { + color: hsl(230, 8%, 24%); +} - .language-json .token.null.keyword { - color: hsl(35, 99%, 36%); - } +.language-json .token.null.keyword { + color: hsl(35, 99%, 36%); +} - /* MD overrides */ - .language-markdown .token.url, - .language-markdown .token.url > .token.operator, - .language-markdown .token.url-reference.url > .token.string { - color: hsl(230, 8%, 24%); - } +/* MD overrides */ +.language-markdown .token.url, +.language-markdown .token.url > .token.operator, +.language-markdown .token.url-reference.url > .token.string { + color: hsl(230, 8%, 24%); +} - .language-markdown .token.url > .token.content { - color: hsl(221, 87%, 60%); - } +.language-markdown .token.url > .token.content { + color: hsl(221, 87%, 60%); +} - .language-markdown .token.url > .token.url, - .language-markdown .token.url-reference.url { - color: hsl(198, 99%, 37%); - } +.language-markdown .token.url > .token.url, +.language-markdown .token.url-reference.url { + color: hsl(198, 99%, 37%); +} - .language-markdown .token.blockquote.punctuation, - .language-markdown .token.hr.punctuation { - color: hsl(230, 4%, 64%); - font-style: italic; - } +.language-markdown .token.blockquote.punctuation, +.language-markdown .token.hr.punctuation { + color: hsl(230, 4%, 64%); + font-style: italic; +} - .language-markdown .token.code-snippet { - color: hsl(119, 34%, 47%); - } +.language-markdown .token.code-snippet { + color: hsl(119, 34%, 47%); +} - .language-markdown .token.bold .token.content { - color: hsl(35, 99%, 36%); - } +.language-markdown .token.bold .token.content { + color: hsl(35, 99%, 36%); +} - .language-markdown .token.italic .token.content { - color: hsl(301, 63%, 40%); - } +.language-markdown .token.italic .token.content { + color: hsl(301, 63%, 40%); +} - .language-markdown .token.strike .token.content, - .language-markdown .token.strike .token.punctuation, - .language-markdown .token.list.punctuation, - .language-markdown .token.title.important > .token.punctuation { - color: hsl(5, 74%, 59%); - } +.language-markdown .token.strike .token.content, +.language-markdown .token.strike .token.punctuation, +.language-markdown .token.list.punctuation, +.language-markdown .token.title.important > .token.punctuation { + color: hsl(5, 74%, 59%); +} - /* General */ - .token.bold { - font-weight: bold; - } +/* General */ +.token.bold { + font-weight: bold; +} - .token.comment, - .token.italic { - font-style: italic; - } +.token.comment, +.token.italic { + font-style: italic; +} - .token.entity { - cursor: help; - } +.token.entity { + cursor: help; +} - .token.namespace { - opacity: 0.8; - } +.token.namespace { + opacity: 0.8; +} - /* Plugin overrides */ - /* Selectors should have higher specificity than those in the plugins' default stylesheets */ +/* Plugin overrides */ +/* Selectors should have higher specificity than those in the plugins' default stylesheets */ - /* Show Invisibles plugin overrides */ - .token.token.tab:not(:empty):before, - .token.token.cr:before, - .token.token.lf:before, - .token.token.space:before { - color: hsla(230, 8%, 24%, 0.2); - } +/* Show Invisibles plugin overrides */ +.token.token.tab:not(:empty):before, +.token.token.cr:before, +.token.token.lf:before, +.token.token.space:before { + color: hsla(230, 8%, 24%, 0.2); +} - /* Toolbar plugin overrides */ - /* Space out all buttons and move them away from the right edge of the code block */ - div.code-toolbar > .toolbar.toolbar > .toolbar-item { - margin-right: 0.4em; - } +/* Toolbar plugin overrides */ +/* Space out all buttons and move them away from the right edge of the code block */ +div.code-toolbar > .toolbar.toolbar > .toolbar-item { + margin-right: 0.4em; +} - /* Styling the buttons */ - div.code-toolbar > .toolbar.toolbar > .toolbar-item > button, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > a, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > span { - background: hsl(230, 1%, 90%); - color: hsl(230, 6%, 44%); - padding: 0.1em 0.4em; - border-radius: 0.3em; - } +/* Styling the buttons */ +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span { + background: hsl(230, 1%, 90%); + color: hsl(230, 6%, 44%); + padding: 0.1em 0.4em; + border-radius: 0.3em; +} - div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover, - div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus { - background: hsl(230, 1%, 78%); /* custom: darken(--syntax-bg, 20%) */ - color: hsl(230, 8%, 24%); - } +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus { + background: hsl(230, 1%, 78%); /* custom: darken(--syntax-bg, 20%) */ + color: hsl(230, 8%, 24%); +} - /* Line Highlight plugin overrides */ - /* The highlighted line itself */ - .line-highlight.line-highlight { - background: hsla(230, 8%, 24%, 0.05); - } +/* Line Highlight plugin overrides */ +/* The highlighted line itself */ +.line-highlight.line-highlight { + background: hsla(230, 8%, 24%, 0.05); +} - /* Default line numbers in Line Highlight plugin */ - .line-highlight.line-highlight:before, - .line-highlight.line-highlight[data-end]:after { - background: hsl(230, 1%, 90%); - color: hsl(230, 8%, 24%); - padding: 0.1em 0.6em; - border-radius: 0.3em; - box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); /* same as Toolbar plugin default */ - } +/* Default line numbers in Line Highlight plugin */ +.line-highlight.line-highlight:before, +.line-highlight.line-highlight[data-end]:after { + background: hsl(230, 1%, 90%); + color: hsl(230, 8%, 24%); + padding: 0.1em 0.6em; + border-radius: 0.3em; + box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); /* same as Toolbar plugin default */ +} - /* Hovering over a linkable line number (in the gutter area) */ - /* Requires Line Numbers plugin as well */ - pre[id].linkable-line-numbers.linkable-line-numbers span.line-numbers-rows > span:hover:before { - background-color: hsla(230, 8%, 24%, 0.05); - } +/* Hovering over a linkable line number (in the gutter area) */ +/* Requires Line Numbers plugin as well */ +pre[id].linkable-line-numbers.linkable-line-numbers span.line-numbers-rows > span:hover:before { + background-color: hsla(230, 8%, 24%, 0.05); +} - /* Line Numbers and Command Line plugins overrides */ - /* Line separating gutter from coding area */ - .line-numbers.line-numbers .line-numbers-rows, - .command-line .command-line-prompt { - border-right-color: hsla(230, 8%, 24%, 0.2); - } +/* Line Numbers and Command Line plugins overrides */ +/* Line separating gutter from coding area */ +.line-numbers.line-numbers .line-numbers-rows, +.command-line .command-line-prompt { + border-right-color: hsla(230, 8%, 24%, 0.2); +} - /* Stuff in the gutter */ - .line-numbers .line-numbers-rows > span:before, - .command-line .command-line-prompt > span:before { - color: hsl(230, 1%, 62%); - } +/* Stuff in the gutter */ +.line-numbers .line-numbers-rows > span:before, +.command-line .command-line-prompt > span:before { + color: hsl(230, 1%, 62%); +} - /* Match Braces plugin overrides */ - /* Note: Outline colour is inherited from the braces */ - .rainbow-braces .token.token.punctuation.brace-level-1, - .rainbow-braces .token.token.punctuation.brace-level-5, - .rainbow-braces .token.token.punctuation.brace-level-9 { - color: hsl(5, 74%, 59%); - } +/* Match Braces plugin overrides */ +/* Note: Outline colour is inherited from the braces */ +.rainbow-braces .token.token.punctuation.brace-level-1, +.rainbow-braces .token.token.punctuation.brace-level-5, +.rainbow-braces .token.token.punctuation.brace-level-9 { + color: hsl(5, 74%, 59%); +} - .rainbow-braces .token.token.punctuation.brace-level-2, - .rainbow-braces .token.token.punctuation.brace-level-6, - .rainbow-braces .token.token.punctuation.brace-level-10 { - color: hsl(119, 34%, 47%); - } +.rainbow-braces .token.token.punctuation.brace-level-2, +.rainbow-braces .token.token.punctuation.brace-level-6, +.rainbow-braces .token.token.punctuation.brace-level-10 { + color: hsl(119, 34%, 47%); +} - .rainbow-braces .token.token.punctuation.brace-level-3, - .rainbow-braces .token.token.punctuation.brace-level-7, - .rainbow-braces .token.token.punctuation.brace-level-11 { - color: hsl(221, 87%, 60%); - } +.rainbow-braces .token.token.punctuation.brace-level-3, +.rainbow-braces .token.token.punctuation.brace-level-7, +.rainbow-braces .token.token.punctuation.brace-level-11 { + color: hsl(221, 87%, 60%); +} - .rainbow-braces .token.token.punctuation.brace-level-4, - .rainbow-braces .token.token.punctuation.brace-level-8, - .rainbow-braces .token.token.punctuation.brace-level-12 { - color: hsl(301, 63%, 40%); - } +.rainbow-braces .token.token.punctuation.brace-level-4, +.rainbow-braces .token.token.punctuation.brace-level-8, +.rainbow-braces .token.token.punctuation.brace-level-12 { + color: hsl(301, 63%, 40%); +} - /* Diff Highlight plugin overrides */ - /* Taken from https://github.com/atom/github/blob/master/styles/variables.less */ - pre.diff-highlight > code .token.token.deleted:not(.prefix), - pre > code.diff-highlight .token.token.deleted:not(.prefix) { - background-color: hsla(353, 100%, 66%, 0.15); - } +/* Diff Highlight plugin overrides */ +/* Taken from https://github.com/atom/github/blob/master/styles/variables.less */ +pre.diff-highlight > code .token.token.deleted:not(.prefix), +pre > code.diff-highlight .token.token.deleted:not(.prefix) { + background-color: hsla(353, 100%, 66%, 0.15); +} - pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection, - pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection, - pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection, - pre > code.diff-highlight .token.token.deleted:not(.prefix) *::-moz-selection { - background-color: hsla(353, 95%, 66%, 0.25); - } +pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection, +pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix) *::-moz-selection { + background-color: hsla(353, 95%, 66%, 0.25); +} - pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection, - pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection, - pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection, - pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection { - background-color: hsla(353, 95%, 66%, 0.25); - } +pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection, +pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection { + background-color: hsla(353, 95%, 66%, 0.25); +} - pre.diff-highlight > code .token.token.inserted:not(.prefix), - pre > code.diff-highlight .token.token.inserted:not(.prefix) { - background-color: hsla(137, 100%, 55%, 0.15); - } +pre.diff-highlight > code .token.token.inserted:not(.prefix), +pre > code.diff-highlight .token.token.inserted:not(.prefix) { + background-color: hsla(137, 100%, 55%, 0.15); +} - pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection, - pre.diff-highlight > code .token.token.inserted:not(.prefix) *::-moz-selection, - pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection, - pre > code.diff-highlight .token.token.inserted:not(.prefix) *::-moz-selection { - background-color: hsla(135, 73%, 55%, 0.25); - } +pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection, +pre.diff-highlight > code .token.token.inserted:not(.prefix) *::-moz-selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix) *::-moz-selection { + background-color: hsla(135, 73%, 55%, 0.25); +} - pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection, - pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection, - pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection, - pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection { - background-color: hsla(135, 73%, 55%, 0.25); - } +pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection, +pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection { + background-color: hsla(135, 73%, 55%, 0.25); +} - /* Previewers plugin overrides */ - /* Based on https://github.com/atom-community/atom-ide-datatip/blob/master/styles/atom-ide-datatips.less and https://github.com/atom/atom/blob/master/packages/one-light-ui */ - /* Border around popup */ - .prism-previewer.prism-previewer:before, - .prism-previewer-gradient.prism-previewer-gradient div { - border-color: hsl(0, 0, 95%); - } +/* Previewers plugin overrides */ +/* Based on https://github.com/atom-community/atom-ide-datatip/blob/master/styles/atom-ide-datatips.less and https://github.com/atom/atom/blob/master/packages/one-light-ui */ +/* Border around popup */ +.prism-previewer.prism-previewer:before, +.prism-previewer-gradient.prism-previewer-gradient div { + border-color: hsl(0, 0, 95%); +} - /* Angle and time should remain as circles and are hence not included */ - .prism-previewer-color.prism-previewer-color:before, - .prism-previewer-gradient.prism-previewer-gradient div, - .prism-previewer-easing.prism-previewer-easing:before { - border-radius: 0.3em; - } +/* Angle and time should remain as circles and are hence not included */ +.prism-previewer-color.prism-previewer-color:before, +.prism-previewer-gradient.prism-previewer-gradient div, +.prism-previewer-easing.prism-previewer-easing:before { + border-radius: 0.3em; +} - /* Triangles pointing to the code */ - .prism-previewer.prism-previewer:after { - border-top-color: hsl(0, 0, 95%); - } +/* Triangles pointing to the code */ +.prism-previewer.prism-previewer:after { + border-top-color: hsl(0, 0, 95%); +} - .prism-previewer-flipped.prism-previewer-flipped.after { - border-bottom-color: hsl(0, 0, 95%); - } +.prism-previewer-flipped.prism-previewer-flipped.after { + border-bottom-color: hsl(0, 0, 95%); +} - /* Background colour within the popup */ - .prism-previewer-angle.prism-previewer-angle:before, - .prism-previewer-time.prism-previewer-time:before, - .prism-previewer-easing.prism-previewer-easing { - background: hsl(0, 0%, 100%); - } +/* Background colour within the popup */ +.prism-previewer-angle.prism-previewer-angle:before, +.prism-previewer-time.prism-previewer-time:before, +.prism-previewer-easing.prism-previewer-easing { + background: hsl(0, 0%, 100%); +} - /* For angle, this is the positive area (eg. 90deg will display one quadrant in this colour) */ - /* For time, this is the alternate colour */ - .prism-previewer-angle.prism-previewer-angle circle, - .prism-previewer-time.prism-previewer-time circle { - stroke: hsl(230, 8%, 24%); - stroke-opacity: 1; - } +/* For angle, this is the positive area (eg. 90deg will display one quadrant in this colour) */ +/* For time, this is the alternate colour */ +.prism-previewer-angle.prism-previewer-angle circle, +.prism-previewer-time.prism-previewer-time circle { + stroke: hsl(230, 8%, 24%); + stroke-opacity: 1; +} - /* Stroke colours of the handle, direction point, and vector itself */ - .prism-previewer-easing.prism-previewer-easing circle, - .prism-previewer-easing.prism-previewer-easing path, - .prism-previewer-easing.prism-previewer-easing line { - stroke: hsl(230, 8%, 24%); - } +/* Stroke colours of the handle, direction point, and vector itself */ +.prism-previewer-easing.prism-previewer-easing circle, +.prism-previewer-easing.prism-previewer-easing path, +.prism-previewer-easing.prism-previewer-easing line { + stroke: hsl(230, 8%, 24%); +} - /* Fill colour of the handle */ - .prism-previewer-easing.prism-previewer-easing circle { - fill: transparent; - } +/* Fill colour of the handle */ +.prism-previewer-easing.prism-previewer-easing circle { + fill: transparent; +} diff --git a/interface/util/Platform.tsx b/interface/util/Platform.tsx index 8d97fcbf0..14c1dff44 100644 --- a/interface/util/Platform.tsx +++ b/interface/util/Platform.tsx @@ -8,6 +8,7 @@ export type Platform = { platform: 'web' | 'tauri'; // This represents the specific platform implementation getThumbnailUrlByThumbKey: (thumbKey: string[]) => string; getFileUrl: (libraryId: string, locationLocalId: number, filePathId: number) => string; + getFileUrlByPath: (path: string) => string; openLink: (url: string) => void; // Tauri patches `window.confirm` to return `Promise` not `bool` confirm(msg: string, cb: (result: boolean) => void): void; @@ -21,12 +22,19 @@ export type Platform = { userHomeDir?(): Promise; // Opens a file path with a given ID openFilePaths?(library: string, ids: number[]): any; + openEphemeralFiles?(paths: string[]): any; revealItems?( library: string, - items: ({ Location: { id: number } } | { FilePath: { id: number } })[] + items: ( + | { Location: { id: number } } + | { FilePath: { id: number } } + | { Ephemeral: { path: string } } + )[] ): Promise; getFilePathOpenWithApps?(library: string, ids: number[]): Promise; + getEphemeralFilesOpenWithApps?(paths: string[]): Promise; openFilePathWith?(library: string, fileIdsAndAppUrls: [number, string][]): Promise; + openEphemeralFileWith?(pathsAndUrls: [string, string][]): Promise; lockAppTheme?(themeType: 'Auto' | 'Light' | 'Dark'): any; auth: { start(key: string): any; diff --git a/packages/client/src/utils/explorerItem.ts b/packages/client/src/utils/explorerItem.ts index 15416e918..52e11011e 100644 --- a/packages/client/src/utils/explorerItem.ts +++ b/packages/client/src/utils/explorerItem.ts @@ -53,9 +53,7 @@ export function getExplorerItemData(data?: null | ExplorerItem) { const location = getItemLocation(data); if (filePath) { itemData.name = filePath.name; - itemData.fullName = `${filePath.name}${ - filePath.extension ? `.${filePath.extension}` : '' - }}`; + itemData.fullName = `${filePath.name}${filePath.extension ? `.${filePath.extension}` : ''}`; itemData.size = byteSize(filePath.size_in_bytes_bytes); itemData.isDir = filePath.is_dir ?? false; itemData.extension = filePath.extension; @@ -131,3 +129,11 @@ export const useItemsAsFilePaths = (items: ExplorerItem[]) => { return array; }, [items]); }; + +export const useItemsAsEphemeralPaths = (items: ExplorerItem[]) => { + return useMemo(() => { + return items + .filter((item) => item.type === 'NonIndexedPath') + .map((item) => item.item as NonIndexedPathItem); + }, [items]); +};