diff --git a/README.md b/README.md index cbfff63..31dc512 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ Qopy is a fixed clipboard manager designed as a simple alternative to the standa - [ ] Metadata for copied items https://github.com/0PandaDEV/Qopy/issues/5 - [ ] Code highlighting https://github.com/0PandaDEV/Qopy/issues/7 - [ ] Streamshare integration https://github.com/0PandaDEV/Qopy/issues/4 +- [ ] Cross-device clipboard sharing https://github.com/0PandaDEV/Qopy/issues/8 If you have ideas for features to include, please write a feature request [here](https://github.com/0pandadev/Qopy/issues). diff --git a/app.vue b/app.vue index 41027d2..72fdf7e 100644 --- a/app.vue +++ b/app.vue @@ -1,434 +1,75 @@ \ No newline at end of file diff --git a/assets/css/style.scss b/assets/css/index.scss similarity index 84% rename from assets/css/style.scss rename to assets/css/index.scss index 28f241b..3390c53 100644 --- a/assets/css/style.scss +++ b/assets/css/index.scss @@ -6,56 +6,6 @@ $text: #E5DFD5; $text2: #ADA9A1; $mutedtext: #78756F; -@font-face { - font-family: SFRoundedRegular; - font-display: swap; - src: url("~/assets/fonts/SFRoundedRegular.otf") format("woff2"); -} - -@font-face { - font-family: SFRoundedMedium; - font-display: swap; - src: url("~/assets/fonts/SFRoundedMedium.otf") format("woff2"); -} - -@font-face { - font-family: SFRoundedSemiBold; - font-display: swap; - src: url("~/assets/fonts/SFRoundedSemiBold.otf") format("woff2"); -} - -@font-face { - font-family: SFMonoRegular; - font-display: swap; - src: url("~/assets/fonts/SFMonoRegular.otf") format("woff2"); -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; - color: $text; - text-decoration: none; - font-family: SFRoundedRegular; - scroll-behavior: smooth; - scrollbar-width: thin; - user-select: none; - - --os-handle-bg: #ADA9A1; - --os-handle-bg-hover: #78756F; - --os-handle-bg-active: #78756F; -} - -html, -body, -#__nuxt { - background-color: transparent; -} - -.os-scrollbar-horizontal { - display: none; -} - .bg { width: 750px; height: 474px; @@ -88,7 +38,7 @@ body, width: 284px; top: 53px; left: 0; - height: calc(100vh - 96px); + height: calc(100vh - 95px); border-right: 1px solid $divider; display: flex; flex-direction: column; @@ -105,7 +55,7 @@ body, padding: 10px; padding-inline: 10px; letter-spacing: 0.5px; - gap: 16px; + gap: 10px; overflow: hidden; text-overflow: clip; white-space: nowrap; @@ -129,12 +79,13 @@ body, } .favicon { - width: 20px; + width: 18px; + height: 18px; } .image { - width: 20px; - height: 20px; + width: 18px; + height: 18px; } .icon { @@ -149,7 +100,7 @@ body, left: 284px; padding: 8px; height: calc(100vh - 96px); - font-family: SFMonoRegular !important; + font-family: CommitMono !important; font-size: 14px; letter-spacing: 1; border-radius: 10px; @@ -159,7 +110,7 @@ body, div { border-radius: 10px; - font-family: SFMonoRegular !important; + font-family: CommitMono !important; } .full-image { @@ -178,12 +129,12 @@ body, } .bottom-bar { - height: 41px; - width: calc(100vw - 3px); + height: 40px; + width: calc(100vw - 2px); backdrop-filter: blur(18px); - background-color: rgba(46, 45, 43, 0.8); + background-color: hsla(40, 3%, 16%, 0.8); position: fixed; - bottom: 2px; + bottom: 1px; left: 1px; z-index: 100; border-radius: 0 0 12px 12px; @@ -191,6 +142,8 @@ body, flex-direction: row; justify-content: space-between; padding-inline: 12px; + padding-right: 6px; + padding-top: 1px; align-items: center; font-size: 14px; border-top: 1px solid $divider; @@ -240,9 +193,10 @@ body, display: flex; align-items: center; gap: 8px; - border-radius: 6px; + border-radius: 7px; background-color: transparent; transition: all .2s; + cursor: pointer; } .paste:hover, diff --git a/assets/css/keybind.scss b/assets/css/keybind.scss new file mode 100644 index 0000000..e1b871a --- /dev/null +++ b/assets/css/keybind.scss @@ -0,0 +1,66 @@ +$primary: #2E2D2B; +$accent: #FEB453; +$divider: #ffffff0d; + +$text: #E5DFD5; +$text2: #ADA9A1; +$mutedtext: #78756F; + +.bg { + width: 750px; + height: 474px; + background-color: $primary; + border: 1px solid $divider; + border-radius: 12px; + z-index: -1; + position: fixed; + outline: none; +} + +.keybind-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 20px; +} + +h2 { + margin-bottom: 20px; +} + +.keybind-input { + width: 300px; + height: 50px; + border: 2px solid $accent; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + cursor: pointer; + margin-bottom: 20px; + background-color: rgba($accent, 0.1); + user-select: none; +} + +.keybind-input:focus { + outline: none; + box-shadow: 0 0 0 2px rgba($accent, 0.5); +} + +button { + padding: 10px 20px; + background-color: $accent; + color: $primary; + border: none; + border-radius: 5px; + font-size: 16px; + cursor: pointer; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/assets/fonts/CommitMono.woff2 b/assets/fonts/CommitMono.woff2 new file mode 100644 index 0000000..f9ac1ee Binary files /dev/null and b/assets/fonts/CommitMono.woff2 differ diff --git a/assets/fonts/SFMonoRegular.otf b/assets/fonts/SFMonoRegular.otf deleted file mode 100644 index 4c6f9f6..0000000 Binary files a/assets/fonts/SFMonoRegular.otf and /dev/null differ diff --git a/components/Icons/Code.vue b/components/Icons/Code.vue index a0a7503..b521a9f 100644 --- a/components/Icons/Code.vue +++ b/components/Icons/Code.vue @@ -1,3 +1,11 @@ \ No newline at end of file diff --git a/components/Icons/File.vue b/components/Icons/File.vue index f65c400..97136c0 100644 --- a/components/Icons/File.vue +++ b/components/Icons/File.vue @@ -1,3 +1,11 @@ \ No newline at end of file diff --git a/components/Icons/Image.vue b/components/Icons/Image.vue index 5903bff..42eca9f 100644 --- a/components/Icons/Image.vue +++ b/components/Icons/Image.vue @@ -1,3 +1,11 @@ \ No newline at end of file diff --git a/components/Icons/Text.vue b/components/Icons/Text.vue index 54aee4b..a35d18e 100644 --- a/components/Icons/Text.vue +++ b/components/Icons/Text.vue @@ -1,3 +1,11 @@ \ No newline at end of file diff --git a/pages/index.vue b/pages/index.vue new file mode 100644 index 0000000..e045e51 --- /dev/null +++ b/pages/index.vue @@ -0,0 +1,459 @@ + + + + + \ No newline at end of file diff --git a/pages/keybind.vue b/pages/keybind.vue new file mode 100644 index 0000000..d9fef25 --- /dev/null +++ b/pages/keybind.vue @@ -0,0 +1,53 @@ + + + + + \ No newline at end of file diff --git a/public/Code.svg b/public/Code.svg deleted file mode 100644 index af5a433..0000000 --- a/public/Code.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/public/Image.svg b/public/Image.svg deleted file mode 100644 index 2a7df95..0000000 --- a/public/Image.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/public/Text.svg b/public/Text.svg deleted file mode 100644 index 2c3d636..0000000 --- a/public/Text.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index d544d17..0000000 --- a/public/file.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 10e711b..883a577 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -28,6 +28,7 @@ "core:window:allow-show", "core:window:allow-set-focus", "core:window:allow-is-focused", - "core:window:allow-is-visible" + "core:window:allow-is-visible", + "fs:allow-read" ] } \ No newline at end of file diff --git a/src-tauri/src/api/clipboard.rs b/src-tauri/src/api/clipboard.rs index e382aa0..c587cb4 100644 --- a/src-tauri/src/api/clipboard.rs +++ b/src-tauri/src/api/clipboard.rs @@ -24,11 +24,12 @@ pub fn set_app_data_dir(path: std::path::PathBuf) { } #[tauri::command] -pub fn read_image(filename: String) -> Result, String> { +pub fn read_image(filename: String) -> Result { let app_data_dir = APP_DATA_DIR.lock().unwrap(); let app_data_dir = app_data_dir.as_ref().expect("App data directory not set"); let image_path = app_data_dir.join("images").join(filename); - fs::read(image_path).map_err(|e| e.to_string()) + let image_data = fs::read(image_path).map_err(|e| e.to_string())?; + Ok(STANDARD.encode(image_data)) } #[tauri::command] @@ -107,26 +108,22 @@ pub fn setup(app: &AppHandle) { let app = app.clone(); runtime.block_on(async move { if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) { - println!("Ignoring programmatic paste"); return; } let clipboard = app.state::(); let available_types = clipboard.available_types().unwrap(); - println!("Clipboard update detected"); - match get_pool(&app).await { Ok(pool) => { if available_types.image { println!("Handling image change"); if let Ok(image_data) = clipboard.read_image_base64() { - let base64_image = STANDARD.encode(&image_data); insert_content_if_not_exists( app.clone(), pool.clone(), "image", - base64_image, + image_data, ) .await; } diff --git a/src-tauri/src/api/hotkeys.rs b/src-tauri/src/api/hotkeys.rs index eaa23e6..ca93955 100644 --- a/src-tauri/src/api/hotkeys.rs +++ b/src-tauri/src/api/hotkeys.rs @@ -1,31 +1,136 @@ -use rdev::{listen, EventType, Key}; -use tauri::Manager; +use rdev::{listen, Event, EventType, Key}; +use tauri::{Manager, Emitter}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::collections::HashSet; +use serde::Serialize; use crate::utils::commands::center_window_on_current_monitor; +static IS_CAPTURING_KEYBIND: AtomicBool = AtomicBool::new(false); + +#[derive(Debug, Clone, Serialize)] +struct CapturedKeybind { + modifiers: Vec, + key: String, +} + +struct KeybindState { + pressed_keys: HashSet, +} + +impl KeybindState { + fn new() -> Self { + Self { + pressed_keys: HashSet::new(), + } + } +} + pub fn setup(app_handle: tauri::AppHandle) { + let app_handle_clone = app_handle.clone(); + let keybind_state = Arc::new(Mutex::new(KeybindState::new())); + std::thread::spawn(move || { - let mut meta_pressed = false; - listen(move |event| { - match event.event_type { - EventType::KeyPress(Key::MetaLeft) | EventType::KeyPress(Key::MetaRight) => { - meta_pressed = true; - } - EventType::KeyRelease(Key::MetaLeft) | EventType::KeyRelease(Key::MetaRight) => { - meta_pressed = false; - } - EventType::KeyPress(Key::KeyV) => { - if meta_pressed { - meta_pressed = false; - let window = app_handle.get_webview_window("main").unwrap(); - window.show().unwrap(); - window.set_focus().unwrap(); - center_window_on_current_monitor(&window); - } - } - _ => {} + if let Err(e) = listen(move |event| { + let mut state = keybind_state.lock().unwrap(); + if IS_CAPTURING_KEYBIND.load(Ordering::SeqCst) { + handle_keybind_capture(&app_handle_clone, event, &mut state); + } else { + handle_normal_hotkey(&app_handle_clone, event, &mut state); } - }) - .unwrap(); + }) { + eprintln!("Error setting up event listener: {:?}", e); + } }); +} + +fn handle_normal_hotkey(app_handle: &tauri::AppHandle, event: Event, state: &mut KeybindState) { + match event.event_type { + EventType::KeyPress(Key::MetaLeft) | EventType::KeyPress(Key::MetaRight) => { + state.pressed_keys.insert(Key::MetaLeft); + } + EventType::KeyRelease(Key::MetaLeft) | EventType::KeyRelease(Key::MetaRight) => { + state.pressed_keys.remove(&Key::MetaLeft); + } + EventType::KeyPress(Key::KeyV) => { + if state.pressed_keys.contains(&Key::MetaLeft) { + state.pressed_keys.clear(); + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + center_window_on_current_monitor(&window); + } + } + } + _ => {} + } +} + +fn handle_keybind_capture(app_handle: &tauri::AppHandle, event: Event, state: &mut KeybindState) { + match event.event_type { + EventType::KeyPress(key) => { + state.pressed_keys.insert(key); + update_captured_keybind(app_handle, &state.pressed_keys); + } + EventType::KeyRelease(key) => { + state.pressed_keys.remove(&key); + } + _ => {} + } +} + +fn update_captured_keybind(app_handle: &tauri::AppHandle, pressed_keys: &HashSet) { + let modifiers: Vec = vec![Key::ControlLeft, Key::ShiftLeft, Key::Alt, Key::MetaLeft] + .into_iter() + .filter(|key| pressed_keys.contains(key)) + .map(|key| key_to_string(key)) + .collect(); + + let key = pressed_keys.iter() + .find(|&&key| !vec![Key::ControlLeft, Key::ShiftLeft, Key::Alt, Key::MetaLeft].contains(&key)) + .map(|&key| key_to_string(key)); + + if let Some(key) = key { + let captured_keybind = CapturedKeybind { + modifiers, + key, + }; + if let Err(e) = app_handle.emit("keybind_captured", captured_keybind) { + eprintln!("Error emitting keybind_captured event: {:?}", e); + } + } +} + +fn key_to_string(key: Key) -> String { + match key { + Key::ControlLeft | Key::ControlRight => "Ctrl".to_string(), + Key::ShiftLeft | Key::ShiftRight => "Shift".to_string(), + Key::Alt => "Alt".to_string(), + Key::MetaLeft | Key::MetaRight => "Meta".to_string(), + _ => format!("{:?}", key), + } +} + +#[tauri::command] +pub fn start_keybind_capture() { + IS_CAPTURING_KEYBIND.store(true, Ordering::SeqCst); +} + +#[tauri::command] +pub fn stop_keybind_capture() { + IS_CAPTURING_KEYBIND.store(false, Ordering::SeqCst); +} + +#[tauri::command] +pub fn get_current_keybind() -> String { + // Implement logic to retrieve the current keybind from your configuration + "Meta+V".to_string() // Placeholder +} + +#[tauri::command] +pub fn save_keybind(keybind: String) -> Result<(), String> { + // Implement logic to save the new keybind to your configuration + println!("Saving keybind: {}", keybind); + Ok(()) } \ No newline at end of file diff --git a/src-tauri/src/api/tray.rs b/src-tauri/src/api/tray.rs index d279774..6b78b8c 100644 --- a/src-tauri/src/api/tray.rs +++ b/src-tauri/src/api/tray.rs @@ -1,13 +1,10 @@ use tauri::{ - Manager, - menu::{MenuBuilder, MenuItemBuilder}, - tray::{MouseButton, TrayIconBuilder, TrayIconEvent}, + menu::{MenuBuilder, MenuItemBuilder}, tray::TrayIconBuilder, Emitter, Manager }; pub fn setup(app: &mut tauri::App) -> Result<(), Box> { let window = app.get_webview_window("main").unwrap(); let window_clone_for_tray = window.clone(); - let window_clone_for_click = window.clone(); let icon_bytes = include_bytes!("../../icons/Square71x71Logo.png"); let icon = tauri::image::Image::from_bytes(icon_bytes).unwrap(); @@ -15,7 +12,11 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box> { let _tray = TrayIconBuilder::new() .menu( &MenuBuilder::new(app) + .items(&[&MenuItemBuilder::with_id("app_name", "Qopy") + .enabled(false) + .build(app)?]) .items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?]) + .items(&[&MenuItemBuilder::with_id("keybind", "Change keybind").build(app)?]) .items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?]) .build()?, ) @@ -31,23 +32,15 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box> { window_clone_for_tray.show().unwrap(); window_clone_for_tray.set_focus().unwrap(); } + window_clone_for_tray.emit("main_route", ()).unwrap(); + } + "keybind" => { + window_clone_for_tray.emit("change_keybind", ()).unwrap(); } _ => (), }) - .on_tray_icon_event(move |_tray, event| { - if let TrayIconEvent::Click { button, .. } = event { - if button == MouseButton::Left { - let is_visible = window_clone_for_click.is_visible().unwrap(); - if is_visible { - window_clone_for_click.hide().unwrap(); - } else { - window_clone_for_click.show().unwrap(); - } - } - } - }) .icon(icon) .build(app)?; Ok(()) -} \ No newline at end of file +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d55dc7e..07b47a8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -31,6 +31,7 @@ fn main() { .setup(|app| { let app_handle = app.handle().clone(); + // #[cfg(not(target_os = "macos"))] api::hotkeys::setup(app_handle.clone()); api::tray::setup(app)?; api::database::setup(app)?; @@ -73,6 +74,10 @@ fn main() { api::clipboard::get_image_path, api::clipboard::write_and_paste, api::clipboard::read_image, + api::hotkeys::start_keybind_capture, + api::hotkeys::stop_keybind_capture, + api::hotkeys::get_current_keybind, + api::hotkeys::save_keybind, ]) .run(tauri::generate_context!()) .expect("error while running tauri application");