diff --git a/src-tauri/common/src/clipboard.rs b/src-tauri/common/src/clipboard.rs new file mode 100644 index 00000000..bdf88870 --- /dev/null +++ b/src-tauri/common/src/clipboard.rs @@ -0,0 +1,44 @@ +use crate::{constants::MAX_TEXT_PREVIEW, types::orm_query::ClipboardWithRelations}; + +pub fn trim_clipboard_data( + mut clipboards: Vec, +) -> Vec { + for clipboard in &mut clipboards { + // Trim text content + if let Some(text) = &mut clipboard.text { + text.data = truncate_text(&text.data, MAX_TEXT_PREVIEW); + } + + // Trim HTML content + if let Some(html) = &mut clipboard.html { + html.data = truncate_text(&html.data, MAX_TEXT_PREVIEW); + } + + // Trim RTF content + if let Some(rtf) = &mut clipboard.rtf { + rtf.data = truncate_text(&rtf.data, MAX_TEXT_PREVIEW); + } + + // Remove image binary data but keep metadata + if let Some(image) = &mut clipboard.image { + image.data = Vec::new(); // Clear binary data + // Thumbnail, dimensions, size etc are preserved + } + + // Clear file binary data but keep metadata + for file in &mut clipboard.files { + file.data = Vec::new(); // Clear binary data + // Name, extension, size etc are preserved + } + } + + clipboards +} + +fn truncate_text(text: &str, max_length: usize) -> String { + if text.len() <= max_length { + text.to_string() + } else { + format!("{}...", &text[..max_length]) + } +} diff --git a/src-tauri/common/src/constants.rs b/src-tauri/common/src/constants.rs index 6a798501..8c3e6614 100644 --- a/src-tauri/common/src/constants.rs +++ b/src-tauri/common/src/constants.rs @@ -19,6 +19,7 @@ pub static SETTINGS_WINDOW_X: i32 = 500; pub static SETTINGS_WINDOW_Y: i32 = 450; pub static MAX_IMAGE_DIMENSIONS: u32 = 1280; +pub static MAX_TEXT_PREVIEW: usize = 500; // Adjust preview length as needed pub static DISPLAY_SCALE: f32 = 1.0; pub static DISPLAY_SCALE_MIN: f32 = 0.5; diff --git a/src-tauri/common/src/lib.rs b/src-tauri/common/src/lib.rs index 2d5a3bb8..a24eeca4 100644 --- a/src-tauri/common/src/lib.rs +++ b/src-tauri/common/src/lib.rs @@ -2,4 +2,5 @@ pub mod keyboard; pub mod language; pub mod macros; pub mod types; +pub mod clipboard; pub mod constants; \ No newline at end of file diff --git a/src-tauri/common/src/types/enums.rs b/src-tauri/common/src/types/enums.rs index a6f723cf..ca659528 100644 --- a/src-tauri/common/src/types/enums.rs +++ b/src-tauri/common/src/types/enums.rs @@ -3,6 +3,15 @@ use sea_orm::{sea_query, EnumIter}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value as JsonValue}; +#[derive(Iden, EnumIter, PartialEq, Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FolderLocation { + #[iden = "database"] + Database, + #[iden = "config"] + Config, +} + #[derive(Iden, EnumIter, PartialEq, Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "lowercase")] pub enum Language { @@ -21,12 +30,14 @@ pub enum Language { pub enum ListenEvent { #[iden = "init"] Init, - #[iden = "set_global_hotkey_event"] - SetGlobalHotkeyEvent, + #[iden = "enable_global_hotkey_event"] + EnableGlobalHotkeyEvent, #[iden = "change_tab"] ChangeTab, #[iden = "scroll_to_top"] ScrollToTop, + #[iden = "new_clipboard"] + NewClipboard, } #[derive(Iden, EnumIter, PartialEq, Serialize, Deserialize, Debug, Clone)] diff --git a/src-tauri/common/src/types/orm_query.rs b/src-tauri/common/src/types/orm_query.rs index d5b67f29..54d97632 100644 --- a/src-tauri/common/src/types/orm_query.rs +++ b/src-tauri/common/src/types/orm_query.rs @@ -14,6 +14,13 @@ pub struct ClipboardWithRelations { pub files: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClipboardsResponse { + pub clipboards: Vec, + pub total: u64, + pub has_more: bool, +} + #[derive(Debug, Clone)] pub struct ClipboardManager { pub clipboard_model: entity::clipboard::ActiveModel, diff --git a/src-tauri/src/commands/clipboard.rs b/src-tauri/src/commands/clipboard.rs index f823d2d2..d88144d1 100644 --- a/src-tauri/src/commands/clipboard.rs +++ b/src-tauri/src/commands/clipboard.rs @@ -2,13 +2,17 @@ extern crate alloc; extern crate image; use crate::{ service::clipboard::{ - clear_clipboards_db, copy_clipboard_from_id, delete_clipboard_db, get_clipboard_db, - get_clipboards_db, star_clipboard_db, + clear_clipboards_db, copy_clipboard_from_id, delete_clipboard_db, get_clipboard_count_db, + get_clipboard_db, get_clipboards_db, star_clipboard_db, }, tauri_config::config::APP, utils::hotkey_manager::unregister_hotkeys, }; -use common::types::{enums::ClipboardType, orm_query::ClipboardWithRelations, types::CommandError}; +use common::{ + clipboard::trim_clipboard_data, + printlog, + types::{enums::ClipboardType, orm_query::ClipboardsResponse, types::CommandError}, +}; use std::fs::File; use tauri::Manager; @@ -18,8 +22,26 @@ pub async fn get_clipboards( search: Option, star: Option, img: Option, -) -> Result, CommandError> { - Ok(get_clipboards_db(cursor, search, star, img).await?) +) -> Result { + let clipboards = get_clipboards_db(cursor, search, star, img).await?; + let total = get_clipboard_count_db().await?; + + // Calculate if there are more items + let current_position = cursor.unwrap_or(0) + clipboards.len() as u64; + let has_more = current_position < total; + + printlog!( + "Total: {}, Current Position: {}, Has More: {}", + total, + current_position, + has_more + ); + + Ok(ClipboardsResponse { + clipboards: trim_clipboard_data(clipboards), + total, + has_more, + }) } #[tauri::command] diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs index 1bb63602..7bde68c0 100644 --- a/src-tauri/src/commands/window.rs +++ b/src-tauri/src/commands/window.rs @@ -2,10 +2,13 @@ use crate::service::{ clipboard::count_clipboards_db, settings::get_data_path, window::open_window, }; use common::types::{ - enums::WebWindow, + enums::{FolderLocation, WebWindow}, types::{CommandError, Config, DatabaseInfo}, }; -use std::fs::{self, read_to_string}; +use std::{ + fs::{self, read_to_string}, + path::PathBuf, +}; use tauri::AppHandle; use tauri_plugin_opener::OpenerExt; @@ -47,3 +50,48 @@ pub async fn get_db_path() -> Result { let config: Config = serde_json::from_str(&read_to_string(&data_path.config_file_path)?)?; Ok(config.db) } + +#[tauri::command] +pub async fn open_folder(location: FolderLocation) -> Result<(), CommandError> { + let data_path = get_data_path(); + + let path = match location { + FolderLocation::Database => { + // Get the database path from config + let config: Config = + serde_json::from_str(&read_to_string(&data_path.config_file_path)?)?; + PathBuf::from(&config.db) + .parent() + .map(|p| p.to_path_buf()) + .ok_or_else(|| { + CommandError::Error("Could not get database directory".to_string()) + })? + } + FolderLocation::Config => PathBuf::from(&data_path.config_path), + }; + + if !path.exists() { + return Err(CommandError::Error("Path does not exist".to_string())); + } + + if !path.is_dir() { + return Err(CommandError::Error("Path is not a directory".to_string())); + } + + #[cfg(target_os = "windows")] + { + std::process::Command::new("explorer").arg(path).spawn()?; + } + + #[cfg(target_os = "macos")] + { + std::process::Command::new("open").arg(path).spawn()?; + } + + #[cfg(target_os = "linux")] + { + std::process::Command::new("xdg-open").arg(path).spawn()?; + } + + Ok(()) +} diff --git a/src-tauri/src/events/hotkey_events.rs b/src-tauri/src/events/hotkey_events.rs index c881ce30..a851d2ad 100644 --- a/src-tauri/src/events/hotkey_events.rs +++ b/src-tauri/src/events/hotkey_events.rs @@ -45,7 +45,6 @@ pub fn init_hotkey_listener() { loop { if let Ok(event) = receiver.try_recv() { if event.state == HotKeyState::Pressed { - printlog!("hotkey caught {:?}", event); let hotkey = get_hotkey_store().get(&event.id).cloned(); if let Some(hotkey) = hotkey { parse_hotkey_event(&hotkey).await; @@ -75,6 +74,9 @@ pub async fn parse_hotkey_event(key: &Key) { get_main_window() .emit(ListenEvent::ScrollToTop.to_string().as_str(), ()) .expect("Failed to emit event"); + get_main_window() + .emit(ListenEvent::Init.to_string().as_str(), ()) + .expect("Failed to emit event"); } Some(HotkeyEvent::TypeClipboard) => { if cfg!(target_os = "linux") { diff --git a/src-tauri/src/events/window_events.rs b/src-tauri/src/events/window_events.rs index b9c73b6f..d769da5b 100644 --- a/src-tauri/src/events/window_events.rs +++ b/src-tauri/src/events/window_events.rs @@ -27,7 +27,7 @@ pub fn init_window_event_listener() { unregister_hotkeys(false); get_main_window() .emit( - ListenEvent::SetGlobalHotkeyEvent.to_string().as_str(), + ListenEvent::EnableGlobalHotkeyEvent.to_string().as_str(), false, ) .expect("failed to emit event"); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ecd79087..d61ac470 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -46,6 +46,7 @@ pub fn run() { window::get_app_version, window::get_db_info, window::get_db_path, + window::open_folder, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/service/clipboard.rs b/src-tauri/src/service/clipboard.rs index 6cbeaf7b..e9e04d84 100644 --- a/src-tauri/src/service/clipboard.rs +++ b/src-tauri/src/service/clipboard.rs @@ -47,6 +47,14 @@ pub async fn load_clipboards_with_relations( .collect() } +pub async fn get_clipboard_count_db() -> Result { + let db = connection::db().await?; + + let count = clipboard::Entity::find().count(&db).await?; + + Ok(count) +} + pub async fn insert_clipboard_db(model: ClipboardManager) -> Result { let db = connection::db().await?; let clipboard = model.clipboard_model.insert(&db).await?; @@ -181,7 +189,7 @@ pub async fn get_clipboards_db( q.filter(f) }) .offset(cursor) - .limit(10) + .limit(25) .order_by_desc(clipboard::Column::Id); Ok(load_clipboards_with_relations(query.all(&db).await?).await) @@ -249,7 +257,7 @@ pub async fn copy_clipboard_from_index(i: u64) -> Result, DbErr> { } pub async fn copy_clipboard_from_id(id: i32, requested_type: ClipboardType) -> Result { - printlog!("type {:?}", requested_type); + printlog!("copy clipboard type: {:?} id:{:?}", requested_type, id); let clipboard_data = get_clipboard_db(id).await?; let clipboard = get_app().state::(); @@ -290,7 +298,7 @@ pub async fn copy_clipboard_from_id(id: i32, requested_type: ClipboardType) -> R } .is_some(); - if success { + if success && !cfg!(debug_assertions) { get_main_window().hide().ok(); } diff --git a/src-tauri/src/service/window.rs b/src-tauri/src/service/window.rs index 1ab451dc..fcb03937 100644 --- a/src-tauri/src/service/window.rs +++ b/src-tauri/src/service/window.rs @@ -49,10 +49,7 @@ pub fn toggle_main_window() { get_main_window().hide().expect("Failed to hide window"); unregister_hotkeys(false); get_main_window() - .emit( - ListenEvent::SetGlobalHotkeyEvent.to_string().as_str(), - false, - ) + .emit(ListenEvent::EnableGlobalHotkeyEvent.to_string().as_str(), false) .expect("Failed to emit set global hotkey event"); } else { position_window_near_cursor(); @@ -66,7 +63,7 @@ pub fn toggle_main_window() { register_hotkeys(true); get_main_window() - .emit(ListenEvent::SetGlobalHotkeyEvent.to_string().as_str(), true) + .emit(ListenEvent::EnableGlobalHotkeyEvent.to_string().as_str(), true) .expect("Failed to emit set global hotkey event"); get_app() diff --git a/src-tauri/src/utils/clipboard_manager.rs b/src-tauri/src/utils/clipboard_manager.rs index ac29070e..19304e94 100644 --- a/src-tauri/src/utils/clipboard_manager.rs +++ b/src-tauri/src/utils/clipboard_manager.rs @@ -60,11 +60,11 @@ impl ClipboardManagerExt for ClipboardManager { .await; if !manager.check_if_last_is_same().await { - insert_clipboard_db(manager) + let clipboard = insert_clipboard_db(manager) .await .expect("Failed to insert"); get_app_window(WebWindow::Main) - .emit(ListenEvent::Init.to_string().as_str(), ()) + .emit(ListenEvent::NewClipboard.to_string().as_str(), clipboard) .expect("Failed to emit"); } } diff --git a/src/components/elements/text-block.tsx b/src/components/elements/text-block.tsx index f9beceb6..fb9b699b 100644 --- a/src/components/elements/text-block.tsx +++ b/src/components/elements/text-block.tsx @@ -6,14 +6,18 @@ interface TextBlockProps { Icon: IconTypes; title: string; className?: string; + header?: JSX.Element; } export const TextBlock: Component = (props) => { return (
-
- -

{props.title}

+
+
+ +

{props.title}

+
+
{props.header}
{props.children}
diff --git a/src/components/elements/toggle.tsx b/src/components/elements/toggle.tsx index 507d2628..e06b07d4 100644 --- a/src/components/elements/toggle.tsx +++ b/src/components/elements/toggle.tsx @@ -17,15 +17,13 @@ export const Toggle: Component = (props) => { class="peer sr-only" />
- {props.checked ? ( - - ) : ( - - )} +
+ {props.checked ? : } +
); diff --git a/src/components/pages/app/app.tsx b/src/components/pages/app/app.tsx index a26a8999..87d9ff05 100644 --- a/src/components/pages/app/app.tsx +++ b/src/components/pages/app/app.tsx @@ -1,13 +1,13 @@ import { BsHddFill } from "solid-icons/bs"; import { FiGlobe } from "solid-icons/fi"; import { Show } from "solid-js"; +import { AppStore } from "../../../store/app-store"; +import { SettingsStore } from "../../../store/settings-store"; import { AppSidebar } from "../../navigation/app-sidebar"; import { ClipboardHistory } from "./clipboard-history"; import { RecentClipboards } from "./recent-clipboards"; import { StarredClipboards } from "./starred-clipboards"; import { ViewMore } from "./view-more"; -import { AppStore } from "../../../store/app-store"; -import { SettingsStore } from "../../../store/settings-store"; function App() { const { settings } = SettingsStore; @@ -19,7 +19,7 @@ function App() {
-
+

{getCurrentTab()?.name?.toUpperCase()}

diff --git a/src/components/pages/app/clipboard/base-clipboard.tsx b/src/components/pages/app/clipboard/base-clipboard.tsx index dabe9235..7258a711 100644 --- a/src/components/pages/app/clipboard/base-clipboard.tsx +++ b/src/components/pages/app/clipboard/base-clipboard.tsx @@ -15,15 +15,20 @@ import { TextClipboard } from "./text-clipboard"; interface BaseClipboardProps { data: ClipboardWithRelations; index: number; + isSelected: boolean; } export const BaseClipboard: Component = (props) => { - const { setClipboards } = ClipboardStore; + const { setClipboards, resetClipboards } = ClipboardStore; const { clipboard } = props.data; const handleDelete = async (id: number) => { if (await invokeCommand(InvokeCommand.DeleteClipboard, { id })) { - setClipboards((prev) => prev.filter((o) => o.clipboard.id !== id)); + setClipboards((prev) => { + const updated = prev.filter((o) => o.clipboard.id !== id); + if (!updated.length) resetClipboards(); + return updated; + }); } }; @@ -64,47 +69,49 @@ export const BaseClipboard: Component = (props) => { }; return ( -
+
{/* Actions overlay */} -
-
- { - e.stopPropagation(); - handleStar(clipboard); - }} - class={`${ - clipboard.star ? "text-yellow-400 dark:text-yellow-300" : "hidden text-zinc-700" - } text-lg hover:text-yellow-400 group-hover:block dark:text-white dark:hover:text-yellow-300`} +
+ { + e.stopPropagation(); + handleStar(clipboard); + }} + title="Star" + class={`${ + clipboard.star ? "text-yellow-400 dark:text-yellow-300" : "hidden text-zinc-700" + } cursor-pointer text-lg hover:text-yellow-400 group-hover:block dark:text-white dark:hover:text-yellow-300`} + /> + {props.data.rtf && ( +
+ )} { e.stopPropagation(); handleDelete(clipboard.id); }} - class="hidden text-lg text-zinc-700 hover:text-red-600 group-hover:block dark:text-white dark:hover:text-red-600" + title="Delete" + class="hidden cursor-pointer text-lg text-zinc-700 hover:text-red-600 group-hover:block dark:text-white dark:hover:text-red-600" />
{/* Content rendered by specific clipboard type */} {clipboard.types.includes(ClipboardType.Image) && } {clipboard.types.includes(ClipboardType.File) && } - {clipboard.types.includes(ClipboardType.Text) && } + {(clipboard.types.includes(ClipboardType.Text) || + clipboard.types.includes(ClipboardType.Html) || + clipboard.types.includes(ClipboardType.Rtf)) && }
); }; diff --git a/src/components/pages/app/clipboard/clipboards.tsx b/src/components/pages/app/clipboard/clipboards.tsx index 51ef9068..612a1958 100644 --- a/src/components/pages/app/clipboard/clipboards.tsx +++ b/src/components/pages/app/clipboard/clipboards.tsx @@ -6,6 +6,7 @@ import { Component, For, Show, createSignal, onMount } from "solid-js"; import clippy from "../../../../assets/clippy.png"; import { ClipboardStore } from "../../../../store/clipboard-store"; import { HotkeyStore } from "../../../../store/hotkey-store"; +import { HotkeyEvent } from "../../../../types/enums"; import { ListenEvent } from "../../../../types/tauri-listen"; import { listenEvent } from "../../../../utils/tauri"; import { BaseClipboard } from "./base-clipboard"; @@ -14,30 +15,36 @@ dayjs.extend(utc); dayjs.extend(relativeTime); export const Clipboards: Component = () => { - const { clipboards, setClipboards, getClipboards, setWhere, clipboardRef, setClipboardRef } = ClipboardStore; const { globalHotkeyEvent, hotkeys } = HotkeyStore; const [scrollToTop, setScrollToTop] = createSignal(false); const onScroll = async () => { - if (!clipboardRef()) return; + if (!ClipboardStore.clipboardRef()) return; const bottom = - clipboardRef() && clipboardRef()!.scrollHeight - clipboardRef()!.scrollTop === clipboardRef()!.clientHeight; + ClipboardStore.clipboardRef() && + ClipboardStore.clipboardRef()!.scrollHeight - ClipboardStore.clipboardRef()!.scrollTop === + ClipboardStore.clipboardRef()!.clientHeight; - clipboardRef()!.scrollTop !== 0 ? setScrollToTop(true) : setScrollToTop(false); + ClipboardStore.clipboardRef()!.scrollTop !== 0 ? setScrollToTop(true) : setScrollToTop(false); - if (bottom) { - setWhere((prev) => ({ ...prev, cursor: clipboards().length })); - const newClipboards = await getClipboards(); - setClipboards((prev) => [...prev, ...newClipboards]); + if (bottom && ClipboardStore.hasMore()) { + ClipboardStore.setWhere((prev) => ({ ...prev, cursor: ClipboardStore.clipboards().length })); + const newClipboards = await ClipboardStore.getClipboards(); + ClipboardStore.setClipboards((prev) => [...prev, ...newClipboards]); } }; - onMount(() => listenEvent(ListenEvent.ScrollToTop, () => clipboardRef()!.scrollTo(0, 0))); + onMount(() => listenEvent(ListenEvent.ScrollToTop, () => ClipboardStore.clipboardRef()!.scrollTo(0, 0))); + + onMount(() => { + window.addEventListener("keydown", ClipboardStore.handleKeyDown); + return () => window.removeEventListener("keydown", ClipboardStore.handleKeyDown); + }); return ( no clipboards @@ -45,26 +52,32 @@ export const Clipboards: Component = () => {
} > -
+
- - {(clipboardData, index) => } + + {(clipboardData, index) => ( + + )}
diff --git a/src/components/pages/app/clipboard/file-clipboard.tsx b/src/components/pages/app/clipboard/file-clipboard.tsx index 5b5812d2..d259f9bf 100644 --- a/src/components/pages/app/clipboard/file-clipboard.tsx +++ b/src/components/pages/app/clipboard/file-clipboard.tsx @@ -1,4 +1,3 @@ -import dayjs from "dayjs"; import { VsFileBinary } from "solid-icons/vs"; import { Component } from "solid-js"; import { ClipboardFileModel, ClipboardWithRelations } from "../../../../types"; @@ -6,7 +5,8 @@ import { ClipboardType } from "../../../../types/enums"; import { InvokeCommand } from "../../../../types/tauri-invoke"; import { formatBytes } from "../../../../utils/helpers"; import { invokeCommand } from "../../../../utils/tauri"; -import { HotkeyNumber } from "../../../utils/hotkey-number"; +import { ClipboardFooter } from "../../../utils/clipboard/clipboard-footer"; +import { ClipboardHeader } from "../../../utils/clipboard/clipboard-header"; interface FileClipboardProps { data: ClipboardWithRelations; @@ -36,7 +36,6 @@ export const FileClipboard: Component = (props) => { }, {} as Record ); - return grouped || {}; }; @@ -52,36 +51,22 @@ export const FileClipboard: Component = (props) => { ); }; diff --git a/src/components/pages/app/clipboard/image-clipboard.tsx b/src/components/pages/app/clipboard/image-clipboard.tsx index 227744d1..4a5c4d10 100644 --- a/src/components/pages/app/clipboard/image-clipboard.tsx +++ b/src/components/pages/app/clipboard/image-clipboard.tsx @@ -1,12 +1,12 @@ import { BsImages } from "solid-icons/bs"; import { Component } from "solid-js"; import { ClipboardWithRelations } from "../../../../types"; -import { formatBytes } from "../../../../utils/helpers"; -import { HotkeyNumber } from "../../../utils/hotkey-number"; -import dayjs from "dayjs"; import { ClipboardType } from "../../../../types/enums"; import { InvokeCommand } from "../../../../types/tauri-invoke"; +import { formatBytes } from "../../../../utils/helpers"; import { invokeCommand } from "../../../../utils/tauri"; +import { ClipboardFooter } from "../../../utils/clipboard/clipboard-footer"; +import { ClipboardHeader } from "../../../utils/clipboard/clipboard-header"; interface ImageClipboardProps { data: ClipboardWithRelations; @@ -34,37 +34,29 @@ export const ImageClipboard: Component = (props) => { await invokeCommand(InvokeCommand.SaveClipboardImage, { id: props.data.clipboard.id }); }; + const imageInfo = + props.data.image && + `${props.data.image.width}x${props.data.image.height} ${formatBytes(Number(props.data.image.size || "0"))}`; + return ( ); }; diff --git a/src/components/pages/app/clipboard/text-clipboard.tsx b/src/components/pages/app/clipboard/text-clipboard.tsx index 49fab406..80cb2587 100644 --- a/src/components/pages/app/clipboard/text-clipboard.tsx +++ b/src/components/pages/app/clipboard/text-clipboard.tsx @@ -1,11 +1,11 @@ import { IoText } from "solid-icons/io"; import { Component } from "solid-js"; import { ClipboardWithRelations } from "../../../../types"; -import { HotkeyNumber } from "../../../utils/hotkey-number"; -import dayjs from "dayjs"; import { ClipboardType } from "../../../../types/enums"; import { InvokeCommand } from "../../../../types/tauri-invoke"; import { invokeCommand } from "../../../../utils/tauri"; +import { ClipboardFooter } from "../../../utils/clipboard/clipboard-footer"; +import { ClipboardHeader } from "../../../utils/clipboard/clipboard-header"; interface TextClipboardProps { data: ClipboardWithRelations; @@ -13,11 +13,23 @@ interface TextClipboardProps { } export const TextClipboard: Component = (props) => { + let type = ClipboardType.Text; + let data = props.data.text?.data; + + if (!props.data.text?.data && props.data.html?.data) { + type = ClipboardType.Html; + data = props.data.html.data; + } + if (!props.data.text?.data && props.data.rtf?.data) { + type = ClipboardType.Rtf; + data = props.data.rtf.data; + } + const handleClick = async (e: MouseEvent) => { e.stopPropagation(); await invokeCommand(InvokeCommand.CopyClipboard, { id: props.data.clipboard.id, - type: ClipboardType.Text, + type, }); }; @@ -25,25 +37,16 @@ export const TextClipboard: Component = (props) => { ); }; diff --git a/src/components/pages/settings/settings-backup.tsx b/src/components/pages/settings/settings-backup.tsx index 540d3f78..ae222084 100644 --- a/src/components/pages/settings/settings-backup.tsx +++ b/src/components/pages/settings/settings-backup.tsx @@ -3,11 +3,12 @@ import { FiUpload } from "solid-icons/fi"; import { RiDeviceSave3Fill } from "solid-icons/ri"; import { TbDatabaseStar } from "solid-icons/tb"; import { Component, Show, createEffect, createSignal, on } from "solid-js"; +import { SettingsStore } from "../../../store/settings-store"; +import { FolderLocation } from "../../../types/enums"; import { InvokeCommand } from "../../../types/tauri-invoke"; import { invokeCommand } from "../../../utils/tauri"; import { TextBlock } from "../../elements/text-block"; import { Toggle } from "../../elements/toggle"; -import { SettingsStore } from "../../../store/settings-store"; interface SettingsBackupProps {} @@ -39,18 +40,23 @@ export const SettingsBackup: Component = ({}) => {
-
- + +
diff --git a/src/components/utils/clipboard/clipboard-footer.tsx b/src/components/utils/clipboard/clipboard-footer.tsx new file mode 100644 index 00000000..f15f589e --- /dev/null +++ b/src/components/utils/clipboard/clipboard-footer.tsx @@ -0,0 +1,19 @@ +import dayjs from "dayjs"; +import { Component } from "solid-js"; +import { ClipboardWithRelations } from "../../../types"; + +interface ClipboardFooterProps { + data: ClipboardWithRelations; + index: number; +} + +export const ClipboardFooter: Component = (props) => { + return ( + <> +
+ {dayjs.utc(props.data.clipboard.created_date).fromNow()} +
+
+ + ); +}; diff --git a/src/components/utils/clipboard/clipboard-header.tsx b/src/components/utils/clipboard/clipboard-header.tsx new file mode 100644 index 00000000..70ad25f4 --- /dev/null +++ b/src/components/utils/clipboard/clipboard-header.tsx @@ -0,0 +1,25 @@ +import { IconTypes } from "solid-icons"; +import { Component, Show } from "solid-js"; +import { HotkeyStore } from "../../../store/hotkey-store"; +import { ClipboardWithRelations } from "../../../types"; + +interface ClipboardHeaderProps { + data: ClipboardWithRelations; + index: number; + Icon: IconTypes; +} + +export const ClipboardHeader: Component = (props) => { + const { globalHotkeyEvent } = HotkeyStore; + + return ( +
+ + +
+ {props.index + 1} +
+
+
+ ); +}; diff --git a/src/components/utils/dark-mode.tsx b/src/components/utils/dark-mode.tsx index 27863351..87e64c1a 100644 --- a/src/components/utils/dark-mode.tsx +++ b/src/components/utils/dark-mode.tsx @@ -12,14 +12,6 @@ export const DarkMode: Component = ({}) => { createEffect(darkMode); return ( - - updateSettings({ - ...settings()!, - dark_mode, - }) - } - /> + updateSettings({ ...settings()!, dark_mode })} /> ); }; diff --git a/src/components/utils/hotkey-number.tsx b/src/components/utils/hotkey-number.tsx deleted file mode 100644 index aedd51cd..00000000 --- a/src/components/utils/hotkey-number.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Component, Show } from "solid-js"; -import { HotkeyStore } from "../../store/hotkey-store"; - -export const HotkeyNumber: Component<{ index: number }> = (props) => { - const { globalHotkeyEvent } = HotkeyStore; - - return ( - -
- {props.index + 1} -
-
- ); -}; diff --git a/src/index.tsx b/src/index.tsx index 5be827fe..25a6a662 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,24 +1,24 @@ import { createResource, onMount } from "solid-js"; import { render } from "solid-js/web"; import App from "./components/pages/app/app"; +import { AppStore } from "./store/app-store"; +import { ClipboardStore } from "./store/clipboard-store"; +import { HotkeyStore } from "./store/hotkey-store"; import "./styles.css"; import { ListenEvent } from "./types/tauri-listen"; import { listenEvent } from "./utils/tauri"; -import { AppStore } from "./store/app-store"; -import { HotkeyStore } from "./store/hotkey-store"; const Index = () => { - const { setGlobalHotkeyEvent } = HotkeyStore; - const { init, setCurrentTab } = AppStore; - - createResource(init); + createResource(AppStore.init); onMount(() => { - listenEvent(ListenEvent.Init, init); + listenEvent(ListenEvent.Init, AppStore.init); + + listenEvent(ListenEvent.EnableGlobalHotkeyEvent, HotkeyStore.setGlobalHotkeyEvent); - listenEvent(ListenEvent.SetGlobalHotkeyEvent, (bool) => setGlobalHotkeyEvent(bool)); + listenEvent(ListenEvent.ChangeTab, AppStore.setCurrentTab); - listenEvent(ListenEvent.ChangeTab, (tab) => setCurrentTab(tab)); + listenEvent(ListenEvent.NewClipboard, ClipboardStore.addClipboard); }); return ; diff --git a/src/settings.tsx b/src/settings.tsx index c8f65fad..9346b38d 100644 --- a/src/settings.tsx +++ b/src/settings.tsx @@ -5,37 +5,34 @@ import { SettingsBackup } from "./components/pages/settings/settings-backup"; import { SettingsGeneral } from "./components/pages/settings/settings-general"; import { SettingsHistory } from "./components/pages/settings/settings-history"; import { SettingsHotkeys } from "./components/pages/settings/settings-hotkeys"; +import { AppStore } from "./store/app-store"; +import { SettingsStore } from "./store/settings-store"; import "./styles.css"; import { ListenEvent } from "./types/tauri-listen"; import { listenEvent } from "./utils/tauri"; -import { AppStore } from "./store/app-store"; -import { SettingsStore } from "./store/settings-store"; const Settings = () => { - const { getCurrentTab } = SettingsStore; - const { init } = AppStore; - - createResource(init); + createResource(AppStore.init); - onMount(() => listenEvent(ListenEvent.Init, init)); + onMount(() => listenEvent(ListenEvent.Init, AppStore.init)); return (
- + - + - + - +
diff --git a/src/store/clipboard-store.ts b/src/store/clipboard-store.ts index 05d43817..21ef41ed 100644 --- a/src/store/clipboard-store.ts +++ b/src/store/clipboard-store.ts @@ -14,30 +14,97 @@ function createClipboardStore() { const [clipboardRef, setClipboardRef] = createSignal(); const [clipboards, setClipboards] = createSignal([]); const [where, setWhere] = createSignal(initialWhere); - + const [hasMore, setHasMore] = createSignal(true); const resetWhere = () => setWhere(initialWhere); + const [selectedIndex, setSelectedIndex] = createSignal(-1); const getClipboards = async () => { - const clipboards = await invokeCommand(InvokeCommand.GetClipboards, where()); - return clipboards; + const response = await invokeCommand(InvokeCommand.GetClipboards, where()); + setHasMore(response.has_more); + return response.clipboards; + }; + + const addClipboard = (clipboard: ClipboardWithRelations) => { + setClipboards((prev) => [clipboard, ...prev]); }; const initClipboards = async () => { + setWhere(initialWhere); const clipboards = await getClipboards(); + setClipboards(clipboards); + ClipboardStore.clipboardRef()!.scrollTo(0, 0); + }; + const resetClipboards = async () => { + setWhere(initialWhere); + setHasMore(true); + const clipboards = await getClipboards(); setClipboards(clipboards); }; + const handleKeyDown = async (e: KeyboardEvent) => { + if (!clipboards().length) return; + + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + const isLastItem = selectedIndex() === clipboards().length - 1; + + // If we're at the last item and there's more data, load more + if (isLastItem && hasMore()) { + setWhere((prev) => ({ ...prev, cursor: clipboards().length })); + const newClipboards = await getClipboards(); + setClipboards((prev) => [...prev, ...newClipboards]); + // Move to next item after loading more + setSelectedIndex((prev) => prev + 1); + } else { + // Normal navigation if not at last item or no more data + setSelectedIndex((prev) => (prev < clipboards().length - 1 ? prev + 1 : prev)); + } + + // Ensure selected item is visible + const nextElement = clipboardRef()?.children[selectedIndex() + 1]; + nextElement?.scrollIntoView({ block: "nearest" }); + break; + + case "ArrowUp": + e.preventDefault(); + setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev)); + // Ensure selected item is visible + const prevElement = clipboardRef()?.children[selectedIndex()]; + prevElement?.scrollIntoView({ block: "nearest" }); + break; + + case "Enter": + if (selectedIndex() >= 0) { + const clipboard = clipboards()[selectedIndex()]; + const type = clipboard.clipboard.types[0]; + await invokeCommand(InvokeCommand.CopyClipboard, { + id: clipboard.clipboard.id, + type, + }); + } + break; + } + }; + return { clipboards, + addClipboard, setClipboards, where, setWhere, + hasMore, + setHasMore, + resetClipboards, resetWhere, getClipboards, clipboardRef, setClipboardRef, initClipboards, + handleKeyDown, + selectedIndex, + setSelectedIndex, }; } diff --git a/src/types/enums.ts b/src/types/enums.ts index 0a5ac1ac..df62993c 100644 --- a/src/types/enums.ts +++ b/src/types/enums.ts @@ -4,6 +4,11 @@ export enum WebWindow { Settings = "settings", } +export enum FolderLocation { + Database = "database", + Config = "config" +} + export enum HotkeyEvent { WindowDisplayToggle = "window_display_toggle", TypeClipboard = "type_clipboard", diff --git a/src/types/index.ts b/src/types/index.ts index 2057b719..581ae38b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -84,6 +84,11 @@ export interface ClipboardWithRelations { rtf?: ClipboardRtfModel; files?: ClipboardFileModel[]; } +export interface ClipboardResponse { + clipboards: ClipboardWithRelations[]; + total: number; + has_more: boolean; +} export type Hotkey = { id: number; diff --git a/src/types/tauri-invoke.ts b/src/types/tauri-invoke.ts index 365ad37d..ef3849f7 100644 --- a/src/types/tauri-invoke.ts +++ b/src/types/tauri-invoke.ts @@ -1,5 +1,5 @@ -import { ClipboardWhere, ClipboardWithRelations, DatabaseInfo, Hotkey, Settings } from "."; -import { ClipboardType, WebWindow } from "./enums"; +import { ClipboardResponse, ClipboardWhere, DatabaseInfo, Hotkey, Settings } from "."; +import { ClipboardType, FolderLocation, WebWindow } from "./enums"; export enum InvokeCommand { // Clipboard commands @@ -25,6 +25,7 @@ export enum InvokeCommand { OpenNewWindow = "open_new_window", OpenBrowserUrl = "open_browser_url", ExitApp = "exit_app", + OpenFolder = "open_folder", // App info commands GetAppVersion = "get_app_version", @@ -36,7 +37,7 @@ export interface TauriInvokeCommands { // Clipboard commands [InvokeCommand.GetClipboards]: { args: ClipboardWhere; - return: ClipboardWithRelations[]; + return: ClipboardResponse; }; [InvokeCommand.DeleteClipboard]: { args: { id: number }; @@ -104,6 +105,10 @@ export interface TauriInvokeCommands { args: undefined; return: void; }; + [InvokeCommand.OpenFolder]: { + args: { location: FolderLocation }; + return: void; + }; // App info commands [InvokeCommand.GetAppVersion]: { diff --git a/src/types/tauri-listen.ts b/src/types/tauri-listen.ts index 7f3e745e..0c97e12e 100644 --- a/src/types/tauri-listen.ts +++ b/src/types/tauri-listen.ts @@ -1,15 +1,18 @@ +import { ClipboardWithRelations } from "."; import { Tab } from "../utils/constants"; export enum ListenEvent { Init = "init", - SetGlobalHotkeyEvent = "set_global_hotkey_event", + EnableGlobalHotkeyEvent = "enable_global_hotkey_event", ChangeTab = "change_tab", ScrollToTop = "scroll_to_top", + NewClipboard = "new_clipboard", } export interface TauriListenEvents { [ListenEvent.Init]: void; - [ListenEvent.SetGlobalHotkeyEvent]: boolean; + [ListenEvent.EnableGlobalHotkeyEvent]: boolean; [ListenEvent.ChangeTab]: Tab; [ListenEvent.ScrollToTop]: void; + [ListenEvent.NewClipboard]: ClipboardWithRelations; }