finally image support

This commit is contained in:
pandadev 2024-07-16 23:08:42 +02:00
parent bae359f1cf
commit c67ed49c3d
No known key found for this signature in database
GPG key ID: C39629DACB8E762F
9 changed files with 98 additions and 100 deletions

50
app.vue
View file

@ -1,8 +1,8 @@
<template> <template>
<div class="bg" @keydown.down.prevent="selectNext" @keydown.up.prevent="selectPrevious" <div class="bg" @keydown.down.prevent="selectNext" @keydown.up.prevent="selectPrevious"
@keydown.enter.prevent="pasteSelectedItem" @keydown.esc="hideApp" tabindex="0"> @keydown.enter.prevent="pasteSelectedItem" @keydown.esc="hideApp" tabindex="0">
<input ref="searchInput" v-model="searchQuery" @input="searchHistory" autocorrect="off" autocapitalize="off" spellcheck="false" <input ref="searchInput" v-model="searchQuery" @input="searchHistory" autocorrect="off" autocapitalize="off"
class="search" type="text" placeholder="Type to filter entries..."> spellcheck="false" class="search" type="text" placeholder="Type to filter entries...">
<div class="bottom-bar"> <div class="bottom-bar">
<div class="left"> <div class="left">
<img src="/Logo.svg" alt=""> <img src="/Logo.svg" alt="">
@ -34,14 +34,19 @@
:ref="el => { if (isSelected(groupIndex, index)) selectedElement = el }"> :ref="el => { if (isSelected(groupIndex, index)) selectedElement = el }">
<img v-if="isUrl(item.content)" :src="getFaviconFromDb(item.favicon)" alt="Favicon" class="favicon"> <img v-if="isUrl(item.content)" :src="getFaviconFromDb(item.favicon)" alt="Favicon" class="favicon">
<FileIcon v-else /> <FileIcon v-else />
<img v-if="item.type === 'image'" :src="item.content" alt="Image" class="preview-image"> <img v-if="item.content_type === 'image'"
:src="item.content.startsWith('data:image') ? item.content : `data:image/bmp;base64,${item.content}`"
alt="Image" class="preview-image">
<span v-else>{{ truncateContent(item.content) }}</span> <span v-else>{{ truncateContent(item.content) }}</span>
</div> </div>
</template> </template>
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
<OverlayScrollbarsComponent class="content"> <OverlayScrollbarsComponent class="content">
<img v-if="selectedItem?.type === 'image'" :src="selectedItem.content" alt="Image" class="full-image"> <img v-if="selectedItem?.content_type === 'image'"
<img v-else-if="isYoutubeWatchUrl(selectedItem?.content)" :src="getYoutubeThumbnail(selectedItem.content)" alt="YouTube Thumbnail" class="full-image"> :src="selectedItem.content.startsWith('data:image') ? selectedItem?.content : `data:image/bmp;base64,${selectedItem?.content}`"
alt="Image" class="full-image">
<img v-else-if="isYoutubeWatchUrl(selectedItem?.content)" :src="getYoutubeThumbnail(selectedItem.content)"
alt="YouTube Thumbnail" class="full-image">
<span v-else>{{ selectedItem?.content || '' }}</span> <span v-else>{{ selectedItem?.content || '' }}</span>
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
<Noise /> <Noise />
@ -204,6 +209,15 @@ const refreshHistory = async () => {
await loadMoreHistory(); await loadMoreHistory();
}; };
const onScroll = () => {
const resultsElement = resultsContainer.value.$el;
console.log('Scroll position:', resultsElement.scrollTop, 'Client height:', resultsElement.clientHeight, 'Scroll height:', resultsElement.scrollHeight);
if (resultsElement.scrollTop + resultsElement.clientHeight >= resultsElement.scrollHeight - 10) {
console.log('Scrolled to the end, loading more history...');
loadMoreHistory();
}
};
const loadMoreHistory = async () => { const loadMoreHistory = async () => {
const lastTimestamp = history.value.length > 0 ? history.value[history.value.length - 1].timestamp : '9999-12-31T23:59:59Z'; const lastTimestamp = history.value.length > 0 ? history.value[history.value.length - 1].timestamp : '9999-12-31T23:59:59Z';
const batchSize = 100; const batchSize = 100;
@ -215,17 +229,28 @@ const loadMoreHistory = async () => {
const newItems = rawHistory.map(item => { const newItems = rawHistory.map(item => {
if (item.type === 'image' && !item.content.startsWith('data:image')) { if (item.type === 'image' && !item.content.startsWith('data:image')) {
return { ...item, content: `data:image/png;base64,${item.content}` }; return { ...item, content: `data:image/bmp;base64,${item.content}` };
} }
return item; return item;
}); });
history.value = [...history.value, ...newItems]; history.value = [...history.value, ...newItems];
console.log('Loaded more history:', newItems.length, 'items');
if (newItems.length < batchSize) {
resultsContainer.value.$el.removeEventListener('scroll', onScroll);
}
}; };
onMounted(async () => { onMounted(async () => {
db.value = await Database.load('sqlite:data.db'); db.value = await Database.load('sqlite:data.db');
await refreshHistory();
await listen('tauri://focus', focusSearchInput);
focusSearchInput();
const resultsElement = resultsContainer.value.$el;
resultsElement.addEventListener('scroll', onScroll);
console.log('Scroll event listener added');
onScroll();
if (!await isEnabled()) { if (!await isEnabled()) {
await enable() await enable()
@ -243,20 +268,11 @@ onMounted(async () => {
} else { } else {
app.show() app.show()
isVisible.value = true; isVisible.value = true;
selectedIndex.value = 0; selectedItemIndex.value = 0;
} }
} }
}); });
await listen('tauri://focus', focusSearchInput);
focusSearchInput();
const resultsElement = resultsContainer.value.$el;
resultsElement.addEventListener('scroll', () => {
if (resultsElement.scrollTop + resultsElement.clientHeight >= resultsElement.scrollHeight - 100) {
loadMoreHistory();
}
});
}); });
const hideApp = async () => { const hideApp = async () => {

View file

@ -1,3 +0,0 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

33
src-tauri/Cargo.lock generated
View file

@ -2605,15 +2605,6 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "objc" name = "objc"
version = "0.2.7" version = "0.2.7"
@ -3224,14 +3215,12 @@ dependencies = [
"arboard", "arboard",
"base64 0.22.1", "base64 0.22.1",
"image 0.25.1", "image 0.25.1",
"log",
"rand 0.8.5", "rand 0.8.5",
"rdev", "rdev",
"regex", "regex",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"simplelog",
"sqlx", "sqlx",
"tauri", "tauri",
"tauri-build", "tauri-build",
@ -4010,17 +3999,6 @@ dependencies = [
"quote", "quote",
] ]
[[package]]
name = "simplelog"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
dependencies = [
"log",
"termcolor",
"time",
]
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "0.3.11" version = "0.3.11"
@ -4854,15 +4832,6 @@ dependencies = [
"utf-8", "utf-8",
] ]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "thin-slice" name = "thin-slice"
version = "0.1.1" version = "0.1.1"
@ -4918,9 +4887,7 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa 1.0.11", "itoa 1.0.11",
"libc",
"num-conv", "num-conv",
"num_threads",
"powerfmt", "powerfmt",
"serde", "serde",
"time-core", "time-core",

View file

@ -10,7 +10,7 @@ rust-version = "1.70"
tauri-build = { version = "2.0.0-beta.18", features = [] } tauri-build = { version = "2.0.0-beta.18", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.0.0-beta.23", features = ["unstable", "tray-icon", "image-png"] } tauri = { version = "2.0.0-beta.23", features = ["tray-icon", "image-png"] }
tauri-plugin-sql = {version = "2.0.0-beta.8", features = ["sqlite"] } tauri-plugin-sql = {version = "2.0.0-beta.8", features = ["sqlite"] }
tauri-plugin-clipboard-manager = "2.1.0-beta.5" tauri-plugin-clipboard-manager = "2.1.0-beta.5"
tauri-plugin-global-shortcut = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } tauri-plugin-global-shortcut = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
@ -27,6 +27,4 @@ arboard = "3.4.0"
image = "0.25.1" image = "0.25.1"
reqwest = { version = "0.12.5", features = ["blocking"] } reqwest = { version = "0.12.5", features = ["blocking"] }
url = "2.5.2" url = "2.5.2"
log = "0.4"
simplelog = "0.12.2"
regex = "1" regex = "1"

View file

@ -13,6 +13,9 @@ use url::Url;
use reqwest::Client; use reqwest::Client;
use arboard::Clipboard; use arboard::Clipboard;
use regex::Regex; use regex::Regex;
use image::ImageFormat;
use image::DynamicImage;
use std::io::Cursor;
#[tauri::command] #[tauri::command]
pub fn simulate_paste() { pub fn simulate_paste() {
@ -33,45 +36,58 @@ pub fn simulate_paste() {
pub fn setup(app_handle: tauri::AppHandle) { pub fn setup(app_handle: tauri::AppHandle) {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let mut is_processing = false; let is_processing = std::sync::Arc::new(std::sync::Mutex::new(false));
std::thread::spawn(move || { std::thread::spawn({
listen(move |event| match event.event_type { let is_processing = std::sync::Arc::clone(&is_processing);
EventType::KeyPress(Key::ControlLeft | Key::ControlRight) => { move || {
let _ = tx.send(true); listen(move |event| match event.event_type {
} EventType::KeyPress(Key::ControlLeft | Key::ControlRight) => {
EventType::KeyRelease(Key::KeyC) => { let _ = tx.send(true);
if rx.try_recv().is_ok() && !is_processing {
is_processing = true;
let pool = app_handle.state::<SqlitePool>();
let rt = app_handle.state::<Runtime>();
let mut clipboard = Clipboard::new().unwrap();
if let Ok(content) = clipboard.get_text() {
rt.block_on(async {
insert_content_if_not_exists(&pool, "text", content).await;
});
}
if let Ok(image) = clipboard.get_image() {
rt.block_on(async {
let base64_image = STANDARD.encode(&image.bytes);
insert_content_if_not_exists(&pool, "image", base64_image).await;
});
}
is_processing = false;
} }
} EventType::KeyRelease(Key::KeyC) => {
EventType::KeyRelease(Key::ControlLeft | Key::ControlRight) => { let mut is_processing = is_processing.lock().unwrap();
is_processing = false; if rx.try_recv().is_ok() && !*is_processing {
} *is_processing = true;
_ => {} let pool = app_handle.state::<SqlitePool>();
}) let rt = app_handle.state::<Runtime>();
.unwrap();
let mut clipboard = Clipboard::new().unwrap();
if let Ok(content) = clipboard.get_text() {
rt.block_on(async {
insert_content_if_not_exists(&pool, "text", content).await;
});
}
if let Ok(image) = clipboard.get_image() {
rt.block_on(async {
let png_image = convert_to_png(image.bytes.to_vec());
let base64_image = STANDARD.encode(&png_image);
insert_content_if_not_exists(&pool, "image", base64_image).await;
});
}
*is_processing = false;
}
}
EventType::KeyRelease(Key::ControlLeft | Key::ControlRight) => {
let mut is_processing = is_processing.lock().unwrap();
*is_processing = false;
}
_ => {}
})
.unwrap();
}
}); });
} }
fn convert_to_png(image_bytes: Vec<u8>) -> Vec<u8> {
let img = image::load_from_memory(&image_bytes).unwrap();
let mut png_bytes: Vec<u8> = Vec::new();
img.write_to(&mut Cursor::new(&mut png_bytes), ImageFormat::Png).unwrap();
png_bytes
}
async fn insert_content_if_not_exists(pool: &SqlitePool, content_type: &str, content: String) { async fn insert_content_if_not_exists(pool: &SqlitePool, content_type: &str, content: String) {
let last_content: Option<String> = sqlx::query_scalar( let last_content: Option<String> = sqlx::query_scalar(
"SELECT content FROM history WHERE content_type = ? ORDER BY timestamp DESC LIMIT 1", "SELECT content FROM history WHERE content_type = ? ORDER BY timestamp DESC LIMIT 1",

View file

@ -40,6 +40,13 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
.await .await
.expect("Failed to create table"); .expect("Failed to create table");
sqlx::query(
"CREATE INDEX IF NOT EXISTS idx_timestamp ON history (timestamp)"
)
.execute(&pool)
.await
.expect("Failed to create index");
if is_new_db { if is_new_db {
let id: String = thread_rng() let id: String = thread_rng()
.sample_iter(&Alphanumeric) .sample_iter(&Alphanumeric)

View file

@ -29,7 +29,7 @@ pub fn setup(app_handle: tauri::AppHandle) {
println!("V key pressed"); println!("V key pressed");
if meta_pressed { if meta_pressed {
println!("Meta+V detected"); println!("Meta+V detected");
let window = app_handle.get_window("main").unwrap(); let window = app_handle.get_webview_window("main").unwrap();
let is_visible = window.is_visible().unwrap(); let is_visible = window.is_visible().unwrap();
if is_visible { if is_visible {
println!("Hiding window"); println!("Hiding window");

View file

@ -12,7 +12,7 @@ use tauri::Manager;
use tauri::PhysicalPosition; use tauri::PhysicalPosition;
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
fn center_window_on_current_monitor(window: &tauri::Window) { fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
if let Some(monitor) = window.current_monitor().unwrap() { if let Some(monitor) = window.current_monitor().unwrap() {
let monitor_size = monitor.size(); let monitor_size = monitor.size();
let window_size = window.outer_size().unwrap(); let window_size = window.outer_size().unwrap();
@ -43,9 +43,9 @@ fn main() {
hotkeys::setup(app_handle.clone()); hotkeys::setup(app_handle.clone());
tray::setup(app)?; tray::setup(app)?;
database::setup(app)?; database::setup(app)?;
clipboard::setup(app_handle); clipboard::setup(app_handle.clone());
if let Some(window) = app.get_window("main") { if let Some(window) = app.get_webview_window("main") {
center_window_on_current_monitor(&window); center_window_on_current_monitor(&window);
window.hide().unwrap(); window.hide().unwrap();
} }
@ -60,15 +60,12 @@ fn main() {
Ok(()) Ok(())
}) })
.on_window_event(|app, event| match event { .on_window_event(|app, event| match event {
#[cfg(not(dev))]
tauri::WindowEvent::Focused(false) => { tauri::WindowEvent::Focused(false) => {
println!("Window lost focus"); if let Some(window) = app.get_webview_window("main") {
if let Some(window) = app.get_window("main") {
window.hide().unwrap(); window.hide().unwrap();
} }
} }
tauri::WindowEvent::Focused(true) => {
println!("Window gained focus");
}
_ => {} _ => {}
}) })
.invoke_handler(tauri::generate_handler![clipboard::simulate_paste]) .invoke_handler(tauri::generate_handler![clipboard::simulate_paste])

View file

@ -5,7 +5,7 @@ use tauri::{
}; };
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> { pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let window = app.get_window("main").unwrap(); let window = app.get_webview_window("main").unwrap();
let window_clone_for_tray = window.clone(); let window_clone_for_tray = window.clone();
let window_clone_for_click = window.clone(); let window_clone_for_click = window.clone();